Skip to main content

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}