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}