webgates_core/groups.rs
1//! Group identifiers for group-based authorization decisions.
2//!
3//! Groups model exact membership such as departments, teams, tenants, projects,
4//! or other organizational units. Unlike roles, groups do not imply privilege
5//! ordering. A user is either a member of the group or not.
6//!
7//! # Examples
8//!
9//! Create groups and use them in access policies:
10//!
11//! ```rust
12//! use webgates_core::authz::access_policy::AccessPolicy;
13//! use webgates_core::groups::Group;
14//! use webgates_core::roles::Role;
15//!
16//! let engineering = Group::new("engineering");
17//! let marketing = Group::new("marketing");
18//!
19//! let policy = AccessPolicy::<Role, Group>::require_group(engineering.clone())
20//! .or_require_group(marketing.clone());
21//!
22//! assert_eq!(engineering.name(), "engineering");
23//! assert_eq!(marketing.name(), "marketing");
24//! assert!(!policy.denies_all());
25//! ```
26//!
27//! Common naming patterns:
28//!
29//! ```rust
30//! use webgates_core::groups::Group;
31//!
32//! let departments = vec![
33//! Group::new("engineering"),
34//! Group::new("marketing"),
35//! Group::new("support"),
36//! ];
37//!
38//! let project_groups = vec![
39//! Group::new("project-alpha"),
40//! Group::new("project-beta"),
41//! ];
42//!
43//! let teams = vec![
44//! Group::new("frontend-team"),
45//! Group::new("backend-team"),
46//! Group::new("qa-team"),
47//! ];
48//!
49//! assert_eq!(departments.len(), 3);
50//! assert_eq!(project_groups.len(), 2);
51//! assert_eq!(teams.len(), 3);
52//! ```
53
54use std::fmt::Display;
55use std::str::FromStr;
56
57use serde::{Deserialize, Serialize};
58
59/// A group identifier used for exact membership checks.
60///
61/// Groups are the non-hierarchical companion to roles. Use them when access is
62/// based on belonging to something, such as a department, project, tenant, or
63/// on-call rotation.
64///
65/// # Example
66/// ```rust
67/// use webgates_core::groups::Group;
68///
69/// let engineering = Group::new("engineering");
70/// let backend_team = Group::new("backend-team");
71///
72/// assert_eq!(engineering.name(), "engineering");
73/// assert_eq!(backend_team.name(), "backend-team");
74/// ```
75#[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Clone)]
76#[serde(transparent)]
77pub struct Group(String);
78
79impl Group {
80 /// Creates a new group from its stable name.
81 ///
82 /// The name should be application-defined and stable over time.
83 ///
84 /// # Parameters
85 /// - `group`: Group identifier such as `"engineering"` or `"project-alpha"`.
86 ///
87 /// # Example
88 /// ```rust
89 /// use webgates_core::groups::Group;
90 ///
91 /// let engineering = Group::new("engineering");
92 /// let project_team = Group::new("project-alpha-team");
93 ///
94 /// assert_eq!(engineering.name(), "engineering");
95 /// assert_eq!(project_team.name(), "project-alpha-team");
96 /// ```
97 pub fn new(group: &str) -> Self {
98 Self(group.to_string())
99 }
100
101 /// Returns the stable group identifier.
102 ///
103 /// # Example
104 /// ```rust
105 /// use webgates_core::groups::Group;
106 ///
107 /// let group = Group::new("engineering");
108 ///
109 /// assert_eq!(group.name(), "engineering");
110 /// ```
111 pub fn name(&self) -> &str {
112 &self.0
113 }
114}
115
116impl FromStr for Group {
117 type Err = String;
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 Ok(Group::new(s))
120 }
121}
122
123impl Display for Group {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(f, "{}", self.name())
126 }
127}
128
129/// Trait for types that expose a stable group identifier.
130///
131/// Implement this for your own group types when infrastructure code needs a
132/// canonical string identifier but you do not want to use the built-in [`Group`]
133/// type directly.
134pub trait GroupEntity {
135 /// Returns the unique identifier for this group as `&str`.
136 fn group_id(&self) -> &str;
137}
138
139/// Allows the built-in [`Group`] type to be used wherever a [`GroupEntity`]
140/// is required.
141impl GroupEntity for Group {
142 fn group_id(&self) -> &str {
143 self.name()
144 }
145}