webgates_core/permissions.rs
1//! Deterministic permission identifiers, sets, and validation utilities.
2//!
3//! This module is the main entry point for fine-grained permission handling in
4//! `webgates-core`.
5//!
6//! If roles are too broad for your use case, permissions let you model specific
7//! capabilities such as `"projects:read"`, `"billing:refund"`, or
8//! `"admin:users:delete"`.
9//!
10//! It exposes:
11//! - [`permission_id::PermissionId`] for stable 64-bit permission identifiers
12//! - [`self::Permissions`] for compact granted-permission storage
13//! - [`application_validator::ApplicationValidator`] and [`collision_checker::PermissionCollisionChecker`] for validation
14//! - [`validation_report::ValidationReport`] and [`permission_collision::PermissionCollision`] for validation results
15//! - [`mapping::PermissionMapping`] and [`mapping::PermissionMappingError`] for registry-style lookups
16//! - [`errors::PermissionsError`] for permission-category errors
17//! - [`as_permission_name::AsPermissionName`] for application-defined permission
18//! enums
19//!
20//! Permission names are normalized before hashing, which keeps checks
21//! deterministic across processes and deployments without requiring a central
22//! registry.
23//!
24//! # Examples
25//!
26//! Validate permissions during tests:
27//!
28//! ```rust
29//! webgates_core::validate_permissions![
30//! "read:resource1",
31//! "write:resource1",
32//! "admin:system",
33//! ];
34//! ```
35//!
36//! Build and query a permission set:
37//!
38//! ```rust
39//! use webgates_core::permissions::permission_id::PermissionId;
40//! use webgates_core::permissions::Permissions;
41//!
42//! let mut permissions = Permissions::new();
43//! permissions
44//! .grant("read:resource1")
45//! .grant(PermissionId::from("write:resource1"));
46//!
47//! assert!(permissions.has("read:resource1"));
48//! assert!(permissions.has(PermissionId::from("write:resource1")));
49//!
50//! permissions.revoke("write:resource1");
51//! assert!(!permissions.has("write:resource1"));
52//! ```
53//!
54//! Use permissions in access policies:
55//!
56//! ```rust
57//! use webgates_core::authz::access_policy::AccessPolicy;
58//! use webgates_core::groups::Group;
59//! use webgates_core::permissions::permission_id::PermissionId;
60//! use webgates_core::roles::Role;
61//!
62//! let policy: AccessPolicy<Role, Group> =
63//! AccessPolicy::require_permission(PermissionId::from("read:resource1"));
64//!
65//! assert!(policy.has_requirements());
66//! ```
67//!
68//! Use application-defined permission enums:
69//!
70//! ```rust
71//! use webgates_core::authz::access_policy::AccessPolicy;
72//! use webgates_core::groups::Group;
73//! use webgates_core::permissions::as_permission_name::AsPermissionName;
74//! use webgates_core::permissions::Permissions;
75//! use webgates_core::roles::Role;
76//!
77//! #[derive(Debug)]
78//! enum Api {
79//! Read,
80//! Write,
81//! }
82//!
83//! #[derive(Debug)]
84//! enum AppPermission {
85//! Api(Api),
86//! System(&'static str),
87//! }
88//!
89//! impl AsPermissionName for AppPermission {
90//! fn as_permission_name(&self) -> String {
91//! match self {
92//! AppPermission::Api(api) => format!("api:{:?}", api).to_lowercase(),
93//! AppPermission::System(name) => format!("system:{name}"),
94//! }
95//! }
96//! }
97//!
98//! let mut permissions = Permissions::new();
99//! permissions.grant(&AppPermission::Api(Api::Read));
100//! assert!(permissions.has(&AppPermission::Api(Api::Read)));
101//!
102//! let policy: AccessPolicy<Role, Group> =
103//! AccessPolicy::require_permission(&AppPermission::Api(Api::Read));
104//! assert!(policy.has_requirements());
105//! ```
106
107use permission_id::PermissionId;
108use roaring::RoaringTreemap;
109use serde::{Deserialize, Serialize};
110use std::fmt;
111
112/// High-level builder for validating application permission sets at startup.
113pub mod application_validator;
114/// Trait for application-defined permission enums that produce a name string.
115///
116/// See [`as_permission_name::AsPermissionName`].
117pub mod as_permission_name;
118/// Low-level collision checker for runtime permission validation and analysis.
119pub mod collision_checker;
120/// Permission-category error values.
121pub mod errors;
122/// Registry-style mapping between permission strings and their identifiers.
123pub mod mapping;
124/// Collision record produced when two permission strings share an identifier.
125pub mod permission_collision;
126/// Deterministic 64-bit identifier derived from a normalized permission name.
127pub mod permission_id;
128/// Test-time permission validation macro support.
129///
130/// This module contains the [`validate_permissions!`](crate::validate_permissions)
131/// macro and its supporting documentation.
132pub mod validate_permissions;
133/// Validation outcome produced by the collision checker and application validator.
134///
135/// See [`validation_report::ValidationReport`].
136pub mod validation_report;
137
138/// A set of granted permissions.
139///
140/// Internally this type stores permission IDs in a compressed bitmap for compact
141/// storage and fast membership checks. The public API accepts permission names
142/// or precomputed [`PermissionId`] values while keeping the bitmap representation
143/// internal.
144///
145/// In day-to-day application code, this is the type you use to grant, revoke,
146/// and check fine-grained capabilities.
147///
148/// # Examples
149///
150/// ```rust
151/// use webgates_core::permissions::Permissions;
152///
153/// let mut permissions = Permissions::new();
154/// permissions
155/// .grant("read:profile")
156/// .grant("write:profile")
157/// .grant("delete:profile");
158///
159/// assert!(permissions.has("read:profile"));
160/// assert!(!permissions.has("admin:users"));
161/// assert!(permissions.has_all(["read:profile", "write:profile"]));
162/// assert!(permissions.has_any(["read:profile", "admin:users"]));
163/// ```
164///
165/// Build a set immutably:
166///
167/// ```rust
168/// use webgates_core::permissions::Permissions;
169///
170/// let permissions = Permissions::new()
171/// .with("read:api")
172/// .with("write:api")
173/// .build();
174///
175/// assert!(permissions.has("read:api"));
176/// assert!(permissions.has("write:api"));
177/// ```
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
179pub struct Permissions {
180 bitmap: RoaringTreemap,
181}
182
183impl Permissions {
184 /// Creates a new empty permission set.
185 ///
186 /// # Examples
187 ///
188 /// ```rust
189 /// use webgates_core::permissions::Permissions;
190 ///
191 /// let permissions = Permissions::new();
192 /// assert!(permissions.is_empty());
193 /// ```
194 pub fn new() -> Self {
195 Self {
196 bitmap: RoaringTreemap::new(),
197 }
198 }
199
200 /// Grants a permission to this set.
201 ///
202 /// Returns a mutable reference to self for method chaining.
203 ///
204 /// # Examples
205 ///
206 /// ```rust
207 /// use webgates_core::permissions::permission_id::PermissionId;
208 /// use webgates_core::permissions::Permissions;
209 ///
210 /// let mut permissions = Permissions::new();
211 /// permissions
212 /// .grant("read:profile")
213 /// .grant(PermissionId::from("write:profile"));
214 ///
215 /// assert!(permissions.has("read:profile"));
216 /// assert!(permissions.has(PermissionId::from("write:profile")));
217 /// ```
218 pub fn grant<P>(&mut self, permission: P) -> &mut Self
219 where
220 P: Into<PermissionId>,
221 {
222 let permission_id = permission.into();
223 self.bitmap.insert(permission_id.as_u64());
224 self
225 }
226
227 /// Revokes a permission from this set.
228 ///
229 /// Returns a mutable reference to self for method chaining.
230 ///
231 /// # Examples
232 ///
233 /// ```rust
234 /// use webgates_core::permissions::permission_id::PermissionId;
235 /// use webgates_core::permissions::Permissions;
236 ///
237 /// let mut permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
238 /// permissions.revoke(PermissionId::from("write:profile"));
239 ///
240 /// assert!(permissions.has("read:profile"));
241 /// assert!(!permissions.has("write:profile"));
242 /// ```
243 pub fn revoke<P>(&mut self, permission: P) -> &mut Self
244 where
245 P: Into<PermissionId>,
246 {
247 let permission_id = permission.into();
248 self.bitmap.remove(permission_id.as_u64());
249 self
250 }
251
252 /// Returns `true` when a specific permission is granted.
253 ///
254 /// # Examples
255 ///
256 /// ```rust
257 /// use webgates_core::permissions::permission_id::PermissionId;
258 /// use webgates_core::permissions::Permissions;
259 ///
260 /// let permissions: Permissions = ["read:profile"].into_iter().collect();
261 ///
262 /// assert!(permissions.has("read:profile"));
263 /// assert!(permissions.has(PermissionId::from("read:profile")));
264 /// assert!(!permissions.has("write:profile"));
265 /// ```
266 pub fn has<P>(&self, permission: P) -> bool
267 where
268 P: Into<PermissionId>,
269 {
270 let permission_id = permission.into();
271 self.bitmap.contains(permission_id.as_u64())
272 }
273
274 /// Returns `true` when all specified permissions are granted.
275 ///
276 /// # Examples
277 ///
278 /// ```rust
279 /// use webgates_core::permissions::permission_id::PermissionId;
280 /// use webgates_core::permissions::Permissions;
281 ///
282 /// let permissions: Permissions = [
283 /// "read:profile",
284 /// "write:profile",
285 /// "read:posts",
286 /// ].into_iter().collect();
287 ///
288 /// assert!(permissions.has_all(["read:profile", "write:profile"]));
289 /// assert!(permissions.has_all([PermissionId::from("read:profile")]));
290 /// assert!(!permissions.has_all(["read:profile", "admin:users"]));
291 /// ```
292 pub fn has_all<I, P>(&self, permissions: I) -> bool
293 where
294 I: IntoIterator<Item = P>,
295 P: Into<PermissionId>,
296 {
297 permissions.into_iter().all(|p| self.has(p))
298 }
299
300 /// Returns `true` when any of the specified permissions are granted.
301 ///
302 /// # Examples
303 ///
304 /// ```rust
305 /// use webgates_core::permissions::permission_id::PermissionId;
306 /// use webgates_core::permissions::Permissions;
307 ///
308 /// let permissions: Permissions = ["read:profile"].into_iter().collect();
309 ///
310 /// assert!(permissions.has_any(["read:profile", "write:profile"]));
311 /// assert!(permissions.has_any([PermissionId::from("read:profile")]));
312 /// assert!(!permissions.has_any(["write:profile", "admin:users"]));
313 /// ```
314 pub fn has_any<I, P>(&self, permissions: I) -> bool
315 where
316 I: IntoIterator<Item = P>,
317 P: Into<PermissionId>,
318 {
319 permissions.into_iter().any(|p| self.has(p))
320 }
321
322 /// Returns the number of granted permissions in this set.
323 ///
324 /// # Examples
325 ///
326 /// ```rust
327 /// use webgates_core::permissions::Permissions;
328 ///
329 /// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
330 /// assert_eq!(permissions.len(), 2);
331 /// ```
332 pub fn len(&self) -> usize {
333 self.bitmap.len() as usize
334 }
335
336 /// Returns `true` if the set contains no permissions.
337 ///
338 /// # Examples
339 ///
340 /// ```rust
341 /// use webgates_core::permissions::Permissions;
342 ///
343 /// let permissions = Permissions::new();
344 /// assert!(permissions.is_empty());
345 ///
346 /// let mut permissions = Permissions::new();
347 /// permissions.grant("read:profile");
348 /// assert!(!permissions.is_empty());
349 /// ```
350 pub fn is_empty(&self) -> bool {
351 self.bitmap.is_empty()
352 }
353
354 /// Removes all permissions from this set.
355 ///
356 /// # Examples
357 ///
358 /// ```rust
359 /// use webgates_core::permissions::Permissions;
360 ///
361 /// let mut permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
362 /// assert!(!permissions.is_empty());
363 ///
364 /// permissions.clear();
365 /// assert!(permissions.is_empty());
366 /// ```
367 pub fn clear(&mut self) {
368 self.bitmap.clear();
369 }
370
371 /// Merges another permission set into this one.
372 ///
373 /// After this call, the set contains every permission that exists in either set.
374 ///
375 /// # Examples
376 ///
377 /// ```rust
378 /// use webgates_core::permissions::Permissions;
379 ///
380 /// let mut permissions1: Permissions = ["read:profile"].into_iter().collect();
381 /// let permissions2: Permissions = ["write:profile"].into_iter().collect();
382 ///
383 /// permissions1.union(&permissions2);
384 ///
385 /// assert!(permissions1.has("read:profile"));
386 /// assert!(permissions1.has("write:profile"));
387 /// ```
388 pub fn union(&mut self, other: &Permissions) -> &mut Self {
389 self.bitmap |= &other.bitmap;
390 self
391 }
392
393 /// Intersects this set with another permission set.
394 ///
395 /// After this call, only permissions present in both sets remain.
396 ///
397 /// # Examples
398 ///
399 /// ```rust
400 /// use webgates_core::permissions::Permissions;
401 ///
402 /// let mut permissions1: Permissions = ["read:profile", "write:profile"].into_iter().collect();
403 /// let permissions2: Permissions = ["read:profile", "admin:users"].into_iter().collect();
404 ///
405 /// permissions1.intersection(&permissions2);
406 ///
407 /// assert!(permissions1.has("read:profile"));
408 /// assert!(!permissions1.has("write:profile"));
409 /// assert!(!permissions1.has("admin:users"));
410 /// ```
411 pub fn intersection(&mut self, other: &Permissions) -> &mut Self {
412 self.bitmap &= &other.bitmap;
413 self
414 }
415
416 /// Removes from this set any permissions that also exist in another set.
417 ///
418 /// # Examples
419 ///
420 /// ```rust
421 /// use webgates_core::permissions::Permissions;
422 ///
423 /// let mut permissions1: Permissions = ["read:profile", "write:profile"].into_iter().collect();
424 /// let permissions2: Permissions = ["write:profile"].into_iter().collect();
425 ///
426 /// permissions1.difference(&permissions2);
427 ///
428 /// assert!(permissions1.has("read:profile"));
429 /// assert!(!permissions1.has("write:profile"));
430 /// ```
431 pub fn difference(&mut self, other: &Permissions) -> &mut Self {
432 self.bitmap -= &other.bitmap;
433 self
434 }
435
436 /// Builder-style variant of [`Self::grant`].
437 ///
438 /// Use this when constructing a permission set fluently without mutable access.
439 ///
440 /// # Examples
441 ///
442 /// ```rust
443 /// use webgates_core::permissions::permission_id::PermissionId;
444 /// use webgates_core::permissions::Permissions;
445 ///
446 /// let permissions = Permissions::new()
447 /// .with("read:profile")
448 /// .with(PermissionId::from("write:profile"))
449 /// .build();
450 ///
451 /// assert!(permissions.has("read:profile"));
452 /// assert!(permissions.has("write:profile"));
453 /// ```
454 pub fn with<P>(mut self, permission: P) -> Self
455 where
456 P: Into<PermissionId>,
457 {
458 self.grant(permission);
459 self
460 }
461
462 /// Finalizes the fluent builder pattern.
463 ///
464 /// This returns `self` unchanged and mostly serves readability in builder-style code.
465 ///
466 /// # Examples
467 ///
468 /// ```rust
469 /// use webgates_core::permissions::Permissions;
470 ///
471 /// let permissions = Permissions::new()
472 /// .with("read:profile")
473 /// .with("write:profile")
474 /// .build();
475 /// ```
476 pub fn build(self) -> Self {
477 self
478 }
479
480 /// Returns an iterator over the raw permission IDs in this set.
481 ///
482 /// Use this when you need to inspect all granted permissions or integrate
483 /// with lower-level systems that work with permission IDs directly.
484 ///
485 /// # Examples
486 ///
487 /// ```rust
488 /// use webgates_core::permissions::Permissions;
489 ///
490 /// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
491 /// let ids: Vec<u64> = permissions.iter().collect();
492 /// assert_eq!(ids.len(), 2);
493 /// ```
494 pub fn iter(&self) -> impl Iterator<Item = u64> + '_ {
495 self.bitmap.iter()
496 }
497}
498
499impl Default for Permissions {
500 fn default() -> Self {
501 Self::new()
502 }
503}
504
505impl fmt::Display for Permissions {
506 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507 write!(f, "Permissions({})", self.len())
508 }
509}
510
511impl From<roaring::RoaringTreemap> for Permissions {
512 fn from(bitmap: roaring::RoaringTreemap) -> Self {
513 Self { bitmap }
514 }
515}
516
517impl From<Permissions> for roaring::RoaringTreemap {
518 fn from(permissions: Permissions) -> Self {
519 permissions.bitmap
520 }
521}
522
523impl AsRef<roaring::RoaringTreemap> for Permissions {
524 fn as_ref(&self) -> &roaring::RoaringTreemap {
525 &self.bitmap
526 }
527}
528
529impl<P> std::iter::FromIterator<P> for Permissions
530where
531 P: Into<PermissionId>,
532{
533 /// Creates a permission set from an iterator of permission values.
534 ///
535 /// # Examples
536 ///
537 /// ```rust
538 /// use webgates_core::permissions::Permissions;
539 ///
540 /// let permissions: Permissions = ["read:profile", "write:profile", "read:posts"]
541 /// .into_iter()
542 /// .collect();
543 ///
544 /// assert!(permissions.has("read:profile"));
545 /// assert!(permissions.has("write:profile"));
546 /// assert!(permissions.has("read:posts"));
547 /// ```
548 fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
549 let mut perms = Self::new();
550 for permission in iter {
551 perms.grant(permission);
552 }
553 perms
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::Permissions;
560 use super::as_permission_name::AsPermissionName;
561 use super::permission_id::PermissionId;
562
563 #[test]
564 fn new_permissions_is_empty() {
565 let permissions = Permissions::new();
566
567 assert!(permissions.is_empty());
568 assert_eq!(permissions.len(), 0);
569 }
570
571 #[test]
572 fn grant_and_has_permission() {
573 let mut permissions = Permissions::new();
574 permissions.grant("read:profile");
575
576 assert!(permissions.has("read:profile"));
577 assert!(!permissions.has("write:profile"));
578 assert_eq!(permissions.len(), 1);
579 assert!(!permissions.is_empty());
580 }
581
582 #[test]
583 fn grant_supports_chaining() {
584 let mut permissions = Permissions::new();
585 permissions
586 .grant("read:profile")
587 .grant("write:profile")
588 .grant("delete:profile");
589
590 assert!(permissions.has("read:profile"));
591 assert!(permissions.has("write:profile"));
592 assert!(permissions.has("delete:profile"));
593 assert_eq!(permissions.len(), 3);
594 }
595
596 #[test]
597 fn revoke_permission_removes_granted_entry() {
598 let mut permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
599 permissions.revoke("write:profile");
600
601 assert!(permissions.has("read:profile"));
602 assert!(!permissions.has("write:profile"));
603 assert_eq!(permissions.len(), 1);
604 }
605
606 #[test]
607 fn has_all_permissions_handles_present_and_missing_values() {
608 let permissions: Permissions = ["read:profile", "write:profile", "read:posts"]
609 .into_iter()
610 .collect();
611
612 assert!(permissions.has_all(["read:profile", "write:profile"]));
613 assert!(permissions.has_all(["read:profile"]));
614 assert!(!permissions.has_all(["read:profile", "admin:users"]));
615 assert!(permissions.has_all(Vec::<&str>::new()));
616 }
617
618 #[test]
619 fn has_any_permission_handles_present_and_missing_values() {
620 let permissions: Permissions = ["read:profile"].into_iter().collect();
621
622 assert!(permissions.has_any(["read:profile", "write:profile"]));
623 assert!(permissions.has_any(["write:profile", "read:profile"]));
624 assert!(!permissions.has_any(["write:profile", "admin:users"]));
625 assert!(!permissions.has_any(Vec::<&str>::new()));
626 }
627
628 #[test]
629 fn clear_permissions_removes_all_entries() {
630 let mut permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
631
632 assert!(!permissions.is_empty());
633
634 permissions.clear();
635
636 assert!(permissions.is_empty());
637 assert_eq!(permissions.len(), 0);
638 }
639
640 #[test]
641 fn union_permissions_combines_entries() {
642 let mut permissions1: Permissions = ["read:profile"].into_iter().collect();
643 let permissions2: Permissions = ["write:profile", "read:posts"].into_iter().collect();
644
645 permissions1.union(&permissions2);
646
647 assert!(permissions1.has("read:profile"));
648 assert!(permissions1.has("write:profile"));
649 assert!(permissions1.has("read:posts"));
650 assert_eq!(permissions1.len(), 3);
651 }
652
653 #[test]
654 fn intersection_permissions_keeps_only_shared_entries() {
655 let mut permissions1: Permissions = ["read:profile", "write:profile"].into_iter().collect();
656 let permissions2: Permissions = ["read:profile", "admin:users"].into_iter().collect();
657
658 permissions1.intersection(&permissions2);
659
660 assert!(permissions1.has("read:profile"));
661 assert!(!permissions1.has("write:profile"));
662 assert!(!permissions1.has("admin:users"));
663 assert_eq!(permissions1.len(), 1);
664 }
665
666 #[test]
667 fn difference_permissions_removes_other_entries() {
668 let mut permissions1 = Permissions::from_iter(["read:profile", "write:profile"]);
669 let permissions2 = Permissions::from_iter(["write:profile"]);
670
671 permissions1.difference(&permissions2);
672
673 assert!(permissions1.has("read:profile"));
674 assert!(!permissions1.has("write:profile"));
675 assert_eq!(permissions1.len(), 1);
676 }
677
678 #[test]
679 fn builder_pattern_returns_populated_set() {
680 let permissions = Permissions::new()
681 .with("read:profile")
682 .with("write:profile")
683 .build();
684
685 assert!(permissions.has("read:profile"));
686 assert!(permissions.has("write:profile"));
687 assert_eq!(permissions.len(), 2);
688 }
689
690 #[test]
691 fn from_iter_builds_permissions_from_names() {
692 let permissions: Permissions = ["read:profile", "write:profile", "read:posts"]
693 .into_iter()
694 .collect();
695
696 assert!(permissions.has("read:profile"));
697 assert!(permissions.has("write:profile"));
698 assert!(permissions.has("read:posts"));
699 assert_eq!(permissions.len(), 3);
700 }
701
702 #[test]
703 fn permissions_are_deterministic() {
704 let permissions1 = Permissions::from_iter(["read:profile"]);
705 let permissions2 = Permissions::from_iter(["read:profile"]);
706
707 assert_eq!(permissions1, permissions2);
708 assert!(permissions1.has("read:profile"));
709 assert!(permissions2.has("read:profile"));
710 }
711
712 #[test]
713 fn supports_permission_id_values() {
714 let mut permissions = Permissions::new();
715 let read_profile = PermissionId::from("read:profile");
716
717 permissions.grant(read_profile);
718
719 assert!(permissions.has(read_profile));
720 assert!(permissions.has("read:profile"));
721 }
722
723 #[test]
724 fn supports_custom_permission_name_types() {
725 #[derive(Debug)]
726 enum AppPermission {
727 ReadProfile,
728 WriteProfile,
729 }
730
731 impl AsPermissionName for AppPermission {
732 fn as_permission_name(&self) -> String {
733 match self {
734 AppPermission::ReadProfile => "profile:read".to_string(),
735 AppPermission::WriteProfile => "profile:write".to_string(),
736 }
737 }
738 }
739
740 let mut permissions = Permissions::new();
741 permissions.grant(&AppPermission::ReadProfile);
742 permissions.grant(&AppPermission::WriteProfile);
743
744 assert!(permissions.has(&AppPermission::ReadProfile));
745 assert!(permissions.has("profile:read"));
746 assert!(permissions.has_all([&AppPermission::ReadProfile, &AppPermission::WriteProfile]));
747 }
748
749 #[test]
750 fn display_implementation_reports_count() {
751 let permissions = Permissions::from_iter(["read:profile", "write:profile"]);
752
753 assert_eq!(format!("{permissions}"), "Permissions(2)");
754 }
755}