Skip to main content

reinhardt_auth/core/
permissions_mixin.rs

1use std::collections::HashSet;
2
3/// PermissionsMixin trait - Django's PermissionsMixin equivalent
4///
5/// Provides permission management functionality for user models. This trait can be composed
6/// with `BaseUser` or `FullUser` to add Django-style permission checking capabilities.
7///
8/// # Permission System
9///
10/// Reinhardt's permission system is inspired by Django's:
11/// - Permissions are strings in the format `"app_label.permission_name"` (e.g., `"blog.add_post"`)
12/// - Users can have permissions directly assigned or inherit them from groups
13/// - Superusers automatically have all permissions
14///
15/// # Examples
16///
17/// Implementing PermissionsMixin for a custom user:
18///
19/// ```
20/// use reinhardt_auth::{BaseUser, PermissionsMixin, PasswordHasher};
21/// #[cfg(feature = "argon2-hasher")]
22/// use reinhardt_auth::Argon2Hasher;
23/// use uuid::Uuid;
24/// use chrono::{DateTime, Utc};
25/// use serde::{Serialize, Deserialize};
26///
27/// #[derive(Serialize, Deserialize)]
28/// struct MyUser {
29///     id: Uuid,
30///     email: String,
31///     password_hash: Option<String>,
32///     last_login: Option<DateTime<Utc>>,
33///     is_active: bool,
34///     is_superuser: bool,
35///     user_permissions: Vec<String>,
36///     groups: Vec<String>,
37/// }
38///
39/// #[cfg(feature = "argon2-hasher")]
40/// impl BaseUser for MyUser {
41///     type PrimaryKey = Uuid;
42///     type Hasher = Argon2Hasher;
43///
44///     fn get_username_field() -> &'static str { "email" }
45///     fn get_username(&self) -> &str { &self.email }
46///     fn password_hash(&self) -> Option<&str> { self.password_hash.as_deref() }
47///     fn set_password_hash(&mut self, hash: String) { self.password_hash = Some(hash); }
48///     fn last_login(&self) -> Option<DateTime<Utc>> { self.last_login }
49///     fn set_last_login(&mut self, time: DateTime<Utc>) { self.last_login = Some(time); }
50///     fn is_active(&self) -> bool { self.is_active }
51/// }
52///
53/// impl PermissionsMixin for MyUser {
54///     fn is_superuser(&self) -> bool { self.is_superuser }
55///     fn user_permissions(&self) -> &[String] { &self.user_permissions }
56///     fn groups(&self) -> &[String] { &self.groups }
57/// }
58///
59/// # #[cfg(feature = "argon2-hasher")]
60/// # {
61/// let mut user = MyUser {
62///     id: Uuid::now_v7(),
63///     email: "admin@example.com".to_string(),
64///     password_hash: None,
65///     last_login: None,
66///     is_active: true,
67///     is_superuser: false,
68///     user_permissions: vec!["blog.add_post".to_string(), "blog.edit_post".to_string()],
69///     groups: vec![],
70/// };
71///
72/// assert!(user.has_perm("blog.add_post"));
73/// assert!(user.has_perm("blog.edit_post"));
74/// assert!(!user.has_perm("blog.delete_post"));
75///
76/// // Superusers have all permissions
77/// user.is_superuser = true;
78/// assert!(user.has_perm("blog.delete_post"));
79/// assert!(user.has_perm("any.permission"));
80/// # }
81/// ```
82pub trait PermissionsMixin: Send + Sync {
83	/// Returns whether this user is a superuser
84	///
85	/// Superusers automatically have all permissions without explicit assignment.
86	fn is_superuser(&self) -> bool;
87
88	/// Returns the list of permissions directly assigned to this user
89	///
90	/// Permissions are typically in the format `"app_label.permission_name"`.
91	/// For example: `"blog.add_post"`, `"blog.edit_post"`, `"auth.change_user"`.
92	fn user_permissions(&self) -> &[String];
93
94	/// Returns the list of groups this user belongs to
95	///
96	/// Groups are used to assign permissions to multiple users at once.
97	/// This method returns group names or identifiers.
98	fn groups(&self) -> &[String];
99
100	/// Returns all permissions directly assigned to this user
101	///
102	/// This does not include group permissions. Use `get_all_permissions()` for the complete set.
103	fn get_user_permissions(&self) -> HashSet<String> {
104		self.user_permissions().iter().cloned().collect()
105	}
106
107	/// Returns all permissions from groups this user belongs to
108	///
109	/// When a global `GroupManager` is registered via
110	/// [`register_group_manager`](crate::register_group_manager), this method
111	/// automatically resolves permissions for the groups returned by
112	/// [`groups()`](Self::groups). Otherwise returns an empty set.
113	fn get_group_permissions(&self) -> HashSet<String> {
114		if let Some(manager) = crate::group_management::get_group_manager() {
115			manager.get_permissions_for_groups_sync(self.groups())
116		} else {
117			HashSet::new()
118		}
119	}
120
121	/// Returns all permissions for this user (user permissions + group permissions)
122	///
123	/// This is the union of permissions directly assigned to the user and permissions
124	/// inherited from groups.
125	fn get_all_permissions(&self) -> HashSet<String> {
126		let mut perms = self.get_user_permissions();
127		perms.extend(self.get_group_permissions());
128		perms
129	}
130
131	/// Checks if this user has a specific permission
132	///
133	/// Superusers always return `true` regardless of the permission.
134	/// For other users, checks if the permission exists in their complete permission set.
135	///
136	/// # Arguments
137	///
138	/// * `perm` - Permission string in the format `"app_label.permission_name"`
139	fn has_perm(&self, perm: &str) -> bool {
140		if self.is_superuser() {
141			return true;
142		}
143		self.get_all_permissions().contains(perm)
144	}
145
146	/// Checks if this user has all of the specified permissions
147	///
148	/// Superusers always return `true` regardless of the permissions.
149	/// For other users, checks if all permissions exist in their complete permission set.
150	///
151	/// # Arguments
152	///
153	/// * `perms` - Slice of permission strings
154	fn has_perms(&self, perms: &[&str]) -> bool {
155		if self.is_superuser() {
156			return true;
157		}
158		let all_perms = self.get_all_permissions();
159		perms.iter().all(|p| all_perms.contains(*p))
160	}
161
162	/// Checks if this user has any permission for a specific app/module
163	///
164	/// Superusers always return `true`.
165	/// For other users, checks if they have any permission starting with `"app_label."`.
166	///
167	/// # Arguments
168	///
169	/// * `app_label` - The application label (e.g., `"blog"`, `"auth"`)
170	fn has_module_perms(&self, app_label: &str) -> bool {
171		if self.is_superuser() {
172			return true;
173		}
174		self.get_all_permissions()
175			.iter()
176			.any(|p| p.starts_with(&format!("{}.", app_label)))
177	}
178}