p2panda_auth/
access.rs

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