p2panda_auth/
access.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::cmp::Ordering;
4use std::fmt::Display;
5
6/// The four basic access levels which can be assigned to an actor. Greater access levels are
7/// assumed to also contain all lower ones.
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
9pub enum AccessLevel {
10    /// Permission to sync a data set.
11    Pull,
12
13    /// Permission to read a data set.
14    Read,
15
16    /// Permission to write to a data set.
17    Write,
18
19    /// Permission to apply membership changes to a group.
20    Manage,
21}
22
23/// A level of access with optional conditions which can be assigned to an actor.
24///
25/// Access can be used to understand the rights of an actor to perform actions (request data,
26/// write data, etc..) within a certain data set. Custom conditions can be defined by the user in
27/// order to introduce domain specific access boundaries or integrate with another access token.
28///
29/// For example, a condition to model access boundaries using paths could be introduced where
30/// having access to "/public" gives you access to "/public/stuff" and "/public/other/stuff" but
31/// not "/private" or "/private/stuff".
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct Access<C = ()> {
34    pub conditions: Option<C>,
35    pub level: AccessLevel,
36}
37
38impl<C> Display for Access<C> {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        let s = match self.level {
41            AccessLevel::Pull => "pull",
42            AccessLevel::Read => "read",
43            AccessLevel::Write => "write",
44            AccessLevel::Manage => "manage",
45        };
46
47        write!(f, "{}", s)
48    }
49}
50
51impl<C> Access<C> {
52    /// Pull access level.
53    pub fn pull() -> Self {
54        Self {
55            level: AccessLevel::Pull,
56            conditions: None,
57        }
58    }
59
60    /// Read access level.
61    pub fn read() -> Self {
62        Self {
63            level: AccessLevel::Read,
64            conditions: None,
65        }
66    }
67
68    /// Write access level.
69    pub fn write() -> Self {
70        Self {
71            level: AccessLevel::Write,
72            conditions: None,
73        }
74    }
75
76    /// Manage access level.
77    pub fn manage() -> Self {
78        Self {
79            level: AccessLevel::Manage,
80            conditions: None,
81        }
82    }
83
84    /// Attach conditions to an access level.
85    pub fn with_conditions(mut self, conditions: C) -> Self {
86        self.conditions = Some(conditions);
87        self
88    }
89
90    /// Access level is Pull.
91    pub fn is_pull(&self) -> bool {
92        matches!(self.level, AccessLevel::Pull)
93    }
94
95    /// Access level is Read.
96    pub fn is_read(&self) -> bool {
97        matches!(self.level, AccessLevel::Read)
98    }
99
100    /// Access level is Write.
101    pub fn is_write(&self) -> bool {
102        matches!(self.level, AccessLevel::Write)
103    }
104
105    /// Access level is Manage.
106    pub fn is_manage(&self) -> bool {
107        matches!(self.level, AccessLevel::Manage)
108    }
109}
110
111impl<C: PartialOrd> PartialOrd for Access<C> {
112    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113        match (self.conditions.as_ref(), other.conditions.as_ref()) {
114            // If self and other contain conditions compare them first.
115            (Some(self_cond), Some(other_cond)) => {
116                match self_cond.partial_cmp(other_cond) {
117                    // When conditions are equal or greater then fall back to comparing the access
118                    // level.
119                    Some(Ordering::Greater | Ordering::Equal) => {
120                        match self.level.cmp(&other.level) {
121                            Ordering::Less => Some(Ordering::Less),
122                            Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
123                        }
124                    }
125                    Some(Ordering::Less) => Some(Ordering::Less),
126                    None => None,
127                }
128            }
129            (None, Some(_)) => match self.level.cmp(&other.level) {
130                Ordering::Less => Some(Ordering::Less),
131                Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
132            },
133            _ => Some(self.level.cmp(&other.level)),
134        }
135    }
136}
137
138impl<C: PartialOrd + Eq> Ord for Access<C> {
139    fn cmp(&self, other: &Self) -> Ordering {
140        self.partial_cmp(other).unwrap_or(Ordering::Less)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use std::cmp::Ordering;
147
148    use crate::Access;
149
150    /// Conditions which models access based on paths. Having access to "/public" gives you access
151    /// to "/public/stuff" and "/public/other/stuff" but not "/private" or "/private/stuff".
152    #[derive(Debug, Clone, PartialEq, Eq)]
153    struct PathCondition(String);
154
155    impl PartialOrd for PathCondition {
156        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157            let self_parts: Vec<_> = self.0.split('/').filter(|s| !s.is_empty()).collect();
158            let other_parts: Vec<_> = other.0.split('/').filter(|s| !s.is_empty()).collect();
159
160            let min_len = self_parts.len().min(other_parts.len());
161            let is_prefix = self_parts[..min_len] == other_parts[..min_len];
162
163            if is_prefix {
164                match self_parts.len().cmp(&other_parts.len()) {
165                    Ordering::Less => Some(Ordering::Greater),
166                    Ordering::Equal => Some(Ordering::Equal),
167                    Ordering::Greater => Some(Ordering::Less),
168                }
169            } else {
170                None
171            }
172        }
173    }
174
175    #[test]
176    fn path_condition_comparators() {
177        let root_access = Access::read().with_conditions(PathCondition("/root".to_string()));
178        let private_access =
179            Access::read().with_conditions(PathCondition("/root/private".to_string()));
180        let public_access =
181            Access::read().with_conditions(PathCondition("/root/public".to_string()));
182
183        // Access to "/root" gives access to all sub-paths
184        assert!(root_access >= private_access);
185        assert!(root_access >= public_access);
186
187        // Unrelated paths are not comparable.
188        assert!(!(private_access >= public_access));
189        assert!(!(private_access <= public_access));
190
191        let read_access_to_root =
192            Access::read().with_conditions(PathCondition("/root".to_string()));
193        let requested_write_access_to_sub_path =
194            Access::write().with_conditions(PathCondition("/root/private".to_string()));
195
196        assert!(requested_write_access_to_sub_path < read_access_to_root);
197
198        let unconditional_read = Access::<PathCondition>::read();
199        assert!(unconditional_read > public_access);
200    }
201
202    /// Conditions containing an access expiry timestamp.
203    #[derive(Debug, Clone, PartialOrd, PartialEq, Eq)]
204    struct ExpiryTimestamp(u64);
205
206    #[test]
207    fn expiry_timestamp_access_ordering() {
208        let access_expires_soon = Access::read().with_conditions(ExpiryTimestamp(10));
209        let access_expires_later = Access::read().with_conditions(ExpiryTimestamp(100));
210
211        // access_expires_later grants more access (access valid for longer).
212        assert!(access_expires_later > access_expires_soon);
213
214        // access_expires_soon grants less access (access valid for shorter time).
215        assert!(access_expires_soon < access_expires_later);
216
217        // It's likely access levels will be tested against some kind of request, here we
218        // construct a request that requires that the requestor has access equal or greater than
219        // "Read" which expires at timestamp 50.
220        const NOW: ExpiryTimestamp = ExpiryTimestamp(50);
221        let requested_read_access = Access::read().with_conditions(NOW);
222
223        // This access has already expired, it is less than the requested access, and the request
224        // would be rejected.
225        assert!(access_expires_soon < requested_read_access);
226
227        // This access is still valid, it is greater than the requested access, and the request
228        // would be accepted.
229        assert!(access_expires_later >= requested_read_access);
230
231        // Even though the held access level (Read) is greater than the requested access level (Pull)
232        // the condition has expired and so the held access is still less than the requested and
233        // the request would be rejected.
234        let requested_pull_access = Access::pull().with_conditions(NOW);
235        assert!(access_expires_soon < requested_pull_access);
236
237        // On the other hand, if the condition is still valid, but the requested access level is
238        // greater than the held one, the request will still be rejected.
239        let requested_write_access = Access::write().with_conditions(NOW);
240        assert!(access_expires_later < requested_write_access);
241
242        // An access level without an expiry is greater or equal than one with.
243        let requested_read_access = Access::read().with_conditions(NOW);
244        let access_no_expiry = Access::<ExpiryTimestamp>::read();
245        assert!(access_no_expiry > requested_read_access);
246    }
247}