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