axum_gate/accounts/
mod.rs

1//! Account management and user data structures.
2//!
3//! This module provides the core [`Account`] type and services for managing user accounts,
4//! including creation, deletion, and repository abstractions for data persistence.
5//!
6//! # Quick Start
7//!
8//! ```rust
9//! use axum_gate::accounts::{Account, AccountInsertService};
10//! use axum_gate::prelude::{Role, Group};
11//! use axum_gate::permissions::Permissions;
12//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
13//! use std::sync::Arc;
14//!
15//! # tokio_test::block_on(async {
16//! // Create repositories
17//! let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
18//! let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
19//!
20//! // Create a new account
21//! let account = AccountInsertService::insert("user@example.com", "password")
22//!     .with_roles(vec![Role::User, Role::Reporter])
23//!     .with_groups(vec![Group::new("engineering"), Group::new("backend-team")])
24//!     .with_permissions(Permissions::from_iter(["read:api", "write:docs"]))
25//!     .into_repositories(account_repo, secret_repo)
26//!     .await;
27//! # });
28//! ```
29
30#[cfg(feature = "server")]
31mod server_impl {
32    pub use super::account_delete::AccountDeleteService;
33    pub use super::account_insert::AccountInsertService;
34    pub use super::account_repository::AccountRepository;
35    pub use super::errors::{AccountOperation, AccountsError};
36    #[cfg(feature = "storage-seaorm")]
37    pub use crate::comma_separated_value::CommaSeparatedValue;
38}
39
40#[cfg(feature = "server")]
41pub use server_impl::*;
42
43use crate::authz::AccessHierarchy;
44use crate::permissions::{PermissionId, Permissions};
45use serde::{Deserialize, Serialize};
46use uuid::Uuid;
47
48#[cfg(feature = "server")]
49mod account_delete;
50#[cfg(feature = "server")]
51mod account_insert;
52#[cfg(feature = "server")]
53mod account_repository;
54#[cfg(feature = "server")]
55pub mod errors;
56
57/// An account contains authorization information about a user.
58///
59/// Accounts store user identification, roles, groups, and permissions. They are the
60/// core entity for authorization decisions in axum-gate.
61///
62/// # Creating Accounts
63///
64/// ```rust
65/// use axum_gate::accounts::Account;
66/// use axum_gate::prelude::{Role, Group};
67/// use axum_gate::permissions::Permissions;
68///
69/// // Create a basic account
70/// let account = Account::new("user123", &[Role::User], &[Group::new("staff")]);
71///
72/// // Create account with permissions
73/// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
74/// let account = Account::<Role, Group>::new("admin@example.com", &[Role::Admin], &[])
75///     .with_permissions(permissions);
76/// ```
77///
78/// # Working with Permissions
79///
80/// ```rust
81/// # use axum_gate::accounts::Account;
82/// # use axum_gate::prelude::{Role, Group};
83/// # use axum_gate::permissions::PermissionId;
84/// # let mut account = Account::<Role, Group>::new("user", &[], &[]);
85/// // Grant permissions
86/// account.grant_permission("read:api");
87/// account.grant_permission(PermissionId::from("write:api"));
88///
89/// // Check permissions directly
90/// if account.permissions.has("read:api") {
91///     println!("User can read API");
92/// }
93///
94/// // Revoke permissions
95/// account.revoke_permission("write:api");
96/// ```
97#[derive(Serialize, Deserialize, Clone, Debug)]
98pub struct Account<R, G>
99where
100    R: AccessHierarchy + Eq,
101    G: Eq + Clone,
102{
103    /// The unique identifier of the account generated during registration.
104    ///
105    /// This UUID links the account to its corresponding authentication secret
106    /// in the secret repository. The separation of account data from secrets
107    /// enhances security by allowing different storage backends and access controls.
108    pub account_id: Uuid,
109    /// The user identifier for this account (e.g., email, username).
110    ///
111    /// This should be unique within your application and is typically what users
112    /// provide during login. It's used to look up accounts in the repository.
113    pub user_id: String,
114    /// Roles assigned to this account.
115    ///
116    /// Roles determine what actions a user can perform. If your roles implement
117    /// `AccessHierarchy`, supervisor roles automatically inherit subordinate permissions.
118    pub roles: Vec<R>,
119    /// Groups this account belongs to.
120    ///
121    /// Groups provide another dimension of access control, allowing you to grant
122    /// permissions based on team membership, department, or other organizational units.
123    pub groups: Vec<G>,
124    /// Custom permissions granted to this account.
125    ///
126    /// Uses a compressed bitmap for efficient storage and fast permission checks.
127    /// Permissions are automatically available when referenced by name using
128    /// deterministic hashing - no coordination between nodes required.
129    pub permissions: Permissions,
130}
131
132impl<R, G> Account<R, G>
133where
134    R: AccessHierarchy + Eq + Clone,
135    G: Eq + Clone,
136{
137    /// Creates a new account with the specified user ID, roles, and groups.
138    ///
139    /// A random UUID is automatically generated for the account ID. The account
140    /// starts with no permissions - use `with_permissions()` or `grant_permission()`
141    /// to add them.
142    ///
143    /// # Arguments
144    /// * `user_id` - Unique identifier for the user (e.g., email or username)
145    /// * `roles` - Roles to assign to this account
146    /// * `groups` - Groups this account should belong to
147    ///
148    /// # Example
149    /// ```rust
150    /// use axum_gate::accounts::Account;
151    /// use axum_gate::prelude::{Role, Group};
152    ///
153    /// let account = Account::new(
154    ///     "user@example.com",
155    ///     &[Role::User, Role::Reporter],
156    ///     &[Group::new("engineering"), Group::new("backend-team")]
157    /// );
158    /// ```
159    pub fn new(user_id: &str, roles: &[R], groups: &[G]) -> Self {
160        let roles = roles.to_vec();
161        let groups = groups.to_vec();
162        Self {
163            account_id: Uuid::now_v7(),
164            user_id: user_id.to_owned(),
165            groups,
166            roles,
167            permissions: Permissions::new(),
168        }
169    }
170
171    /// Creates a new account with the specified account ID.
172    ///
173    /// This constructor is primarily used internally when loading accounts from
174    /// repositories. Most applications should use `new()` which generates a random ID.
175    ///
176    /// # Arguments
177    /// * `account_id` - The UUID to use for this account
178    /// * `user_id` - Unique identifier for the user
179    /// * `roles` - Roles to assign to this account
180    /// * `groups` - Groups this account should belong to
181    #[cfg(feature = "storage-seaorm")]
182    pub(crate) fn new_with_account_id(
183        account_id: &Uuid,
184        user_id: &str,
185        roles: &[R],
186        groups: &[G],
187    ) -> Self {
188        let roles = roles.to_vec();
189        let groups = groups.to_vec();
190        Self {
191            account_id: account_id.to_owned(),
192            user_id: user_id.to_owned(),
193            groups,
194            roles,
195            permissions: Permissions::new(),
196        }
197    }
198
199    /// Consumes this account and returns it with the specified permissions.
200    ///
201    /// This is useful when building accounts with specific permission sets.
202    ///
203    /// # Example
204    /// ```rust
205    /// use axum_gate::accounts::Account;
206    /// use axum_gate::prelude::{Role, Group};
207    /// use axum_gate::permissions::Permissions;
208    ///
209    /// // Create permissions
210    /// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
211    /// let account = Account::<Role, Group>::new("user@example.com", &[Role::User], &[])
212    ///     .with_permissions(permissions);
213    /// ```
214    pub fn with_permissions(self, permissions: Permissions) -> Self {
215        Self {
216            permissions,
217            ..self
218        }
219    }
220
221    /// Grants a permission to this account.
222    ///
223    /// # Example
224    /// ```rust
225    /// use axum_gate::accounts::Account;
226    /// use axum_gate::prelude::{Role, Group};
227    /// use axum_gate::permissions::PermissionId;
228    ///
229    /// let mut account = Account::<Role, Group>::new("user", &[], &[]);
230    /// account.grant_permission("read:profile");
231    /// account.grant_permission(PermissionId::from("write:profile"));
232    /// ```
233    pub fn grant_permission<P>(&mut self, permission: P)
234    where
235        P: Into<PermissionId>,
236    {
237        self.permissions.grant(permission);
238    }
239
240    /// Revokes a permission from this account.
241    ///
242    /// # Example
243    /// ```rust
244    /// use axum_gate::accounts::Account;
245    /// use axum_gate::prelude::{Role, Group};
246    /// use axum_gate::permissions::PermissionId;
247    ///
248    /// let mut account = Account::<Role, Group>::new("user", &[], &[]);
249    /// account.grant_permission("write:profile");
250    /// account.revoke_permission(PermissionId::from("write:profile"));
251    /// ```
252    pub fn revoke_permission<P>(&mut self, permission: P)
253    where
254        P: Into<PermissionId>,
255    {
256        self.permissions.revoke(permission);
257    }
258
259    /// Returns true if this account has the given role.
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use axum_gate::accounts::Account;
265    /// use axum_gate::prelude::{Role, Group};
266    ///
267    /// let account = Account::<Role, Group>::new(
268    ///     "user@example.com",
269    ///     &[Role::User],
270    ///     &[Group::new("engineering")]
271    /// );
272    ///
273    /// assert!(account.has_role(&Role::User));
274    /// assert!(!account.has_role(&Role::Admin));
275    /// ```
276    pub fn has_role(&self, role: &R) -> bool {
277        self.roles.contains(role)
278    }
279
280    /// Returns true if this account is a member of the given group.
281    ///
282    /// # Example
283    ///
284    /// ```rust
285    /// use axum_gate::accounts::Account;
286    /// use axum_gate::prelude::{Role, Group};
287    ///
288    /// let account = Account::<Role, Group>::new(
289    ///     "user@example.com",
290    ///     &[Role::User],
291    ///     &[Group::new("engineering")]
292    /// );
293    ///
294    /// assert!(account.is_member_of(&Group::new("engineering")));
295    /// assert!(!account.is_member_of(&Group::new("marketing")));
296    /// ```
297    pub fn is_member_of(&self, group: &G) -> bool {
298        self.groups.contains(group)
299    }
300
301    /// Returns true if this account has the specified permission.
302    ///
303    /// Accepts any type that converts into `PermissionId` (e.g., `&str`, `PermissionId`).
304    ///
305    /// # Example
306    ///
307    /// ```rust
308    /// use axum_gate::accounts::Account;
309    /// use axum_gate::prelude::{Role, Group};
310    /// use axum_gate::permissions::PermissionId;
311    ///
312    /// let mut account = Account::<Role, Group>::new("user@example.com", &[], &[]);
313    /// account.grant_permission("read:api");
314    /// account.grant_permission(PermissionId::from("write:docs"));
315    ///
316    /// assert!(account.has_permission("read:api"));
317    /// assert!(account.has_permission(PermissionId::from("write:docs")));
318    /// assert!(!account.has_permission("admin:system"));
319    /// ```
320    pub fn has_permission<P>(&self, permission: P) -> bool
321    where
322        P: Into<PermissionId>,
323    {
324        self.permissions.has(permission)
325    }
326}
327
328#[cfg(feature = "storage-seaorm")]
329impl<R, G> TryFrom<crate::repositories::sea_orm::models::account::Model> for Account<R, G>
330where
331    R: AccessHierarchy + Eq + std::fmt::Display + Clone,
332    Vec<R>: CommaSeparatedValue,
333    G: Eq + Clone,
334    Vec<G>: CommaSeparatedValue,
335{
336    type Error = String;
337
338    fn try_from(
339        value: crate::repositories::sea_orm::models::account::Model,
340    ) -> Result<Self, Self::Error> {
341        Ok(Self::new_with_account_id(
342            &value.account_id,
343            &value.user_id,
344            &Vec::<R>::from_csv(&value.roles)?,
345            &Vec::<G>::from_csv(&value.groups)?,
346        ))
347    }
348}