Skip to main content

p2panda_auth/
access.rs

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