1use 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#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
16#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
17pub enum AccessLevel {
18 Pull,
20
21 Read,
23
24 Write,
26
27 Manage,
29}
30
31#[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 pub fn pull() -> Self {
63 Self {
64 level: AccessLevel::Pull,
65 conditions: None,
66 }
67 }
68
69 pub fn read() -> Self {
71 Self {
72 level: AccessLevel::Read,
73 conditions: None,
74 }
75 }
76
77 pub fn write() -> Self {
79 Self {
80 level: AccessLevel::Write,
81 conditions: None,
82 }
83 }
84
85 pub fn manage() -> Self {
87 Self {
88 level: AccessLevel::Manage,
89 conditions: None,
90 }
91 }
92
93 pub fn with_conditions(mut self, conditions: C) -> Self {
95 self.conditions = Some(conditions);
96 self
97 }
98
99 pub fn is_pull(&self) -> bool {
101 matches!(self.level, AccessLevel::Pull)
102 }
103
104 pub fn is_read(&self) -> bool {
106 matches!(self.level, AccessLevel::Read)
107 }
108
109 pub fn is_write(&self) -> bool {
111 matches!(self.level, AccessLevel::Write)
112 }
113
114 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 (Some(self_cond), Some(other_cond)) => {
125 match self_cond.partial_cmp(other_cond) {
126 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 #[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 assert!(root_access >= private_access);
216 assert!(root_access >= public_access);
217
218 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 #[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 assert!(access_expires_later > access_expires_soon);
244
245 assert!(access_expires_soon < access_expires_later);
247
248 const NOW: ExpiryTimestamp = ExpiryTimestamp(50);
252 let requested_read_access = Access::read().with_conditions(NOW);
253
254 assert!(access_expires_soon < requested_read_access);
257
258 assert!(access_expires_later >= requested_read_access);
261
262 let requested_pull_access = Access::pull().with_conditions(NOW);
266 assert!(access_expires_soon < requested_pull_access);
267
268 let requested_write_access = Access::write().with_conditions(NOW);
271 assert!(access_expires_later < requested_write_access);
272
273 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}