1use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26use crate::{Domain, Project, System};
27
28#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum AuthTokenScopeError {
34 #[error("Auth data is missing")]
36 MissingAuthData,
37
38 #[error("Cannot determine authorization scope from config")]
40 MissingScope,
41
42 #[error(transparent)]
44 Builder {
46 #[from]
47 source: crate::BuilderError,
48 },
49}
50
51#[derive(Clone, Deserialize, Eq, Hash, PartialEq, Serialize, Debug, Default)]
53#[serde(rename_all = "lowercase")]
54pub enum AuthTokenScope {
55 Project(Project),
57 Domain(Domain),
59 System(System),
61 #[default]
63 Unscoped,
64}
65
66impl AuthTokenScope {
67 pub fn matches(&self, cached: &Self) -> bool {
71 match (self, cached) {
72 (AuthTokenScope::Project(req), AuthTokenScope::Project(cached)) => {
73 let id_match = req
74 .id
75 .as_ref()
76 .is_none_or(|id| cached.id.as_ref() == Some(id));
77 let name_match = req
78 .name
79 .as_ref()
80 .is_none_or(|name| cached.name.as_ref() == Some(name));
81 let domain_match = if let Some(req_domain) = &req.domain {
82 if let Some(cached_domain) = &cached.domain {
83 let d_id_match = req_domain
84 .id
85 .as_ref()
86 .is_none_or(|id| cached_domain.id.as_ref() == Some(id));
87 let d_name_match = req_domain
88 .name
89 .as_ref()
90 .is_none_or(|name| cached_domain.name.as_ref() == Some(name));
91 d_id_match && d_name_match
92 } else {
93 true
94 }
95 } else {
96 true
97 };
98 id_match && name_match && domain_match
99 }
100 (AuthTokenScope::Domain(req), AuthTokenScope::Domain(cached)) => {
101 let id_match = req
102 .id
103 .as_ref()
104 .is_none_or(|id| cached.id.as_ref() == Some(id));
105 let name_match = req
106 .name
107 .as_ref()
108 .is_none_or(|name| cached.name.as_ref() == Some(name));
109 id_match && name_match
110 }
111 (AuthTokenScope::Unscoped, AuthTokenScope::Unscoped) => true,
112 (AuthTokenScope::System(_), AuthTokenScope::System(_)) => true,
113 _ => false,
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn make_project_scope(id: Option<&str>, name: Option<&str>) -> AuthTokenScope {
123 AuthTokenScope::Project(Project {
124 id: id.map(|s| s.to_string()),
125 name: name.map(|s| s.to_string()),
126 domain: None,
127 })
128 }
129
130 fn make_domain_scope(id: Option<&str>, name: Option<&str>) -> AuthTokenScope {
131 AuthTokenScope::Domain(Domain {
132 id: id.map(|s| s.to_string()),
133 name: name.map(|s| s.to_string()),
134 })
135 }
136
137 #[test]
139 fn test_matches_project_exact_id() {
140 let req = make_project_scope(Some("proj-1"), None);
141 let cached = make_project_scope(Some("proj-1"), None);
142 assert!(req.matches(&cached));
143 }
144
145 #[test]
146 fn test_matches_project_id_mismatch() {
147 let req = make_project_scope(Some("proj-1"), None);
148 let cached = make_project_scope(Some("proj-2"), None);
149 assert!(!req.matches(&cached));
150 }
151
152 #[test]
153 fn test_matches_project_req_id_none_matches_cached_id_some() {
154 let req = make_project_scope(None, Some("myproj"));
155 let cached = make_project_scope(Some("proj-1"), Some("myproj"));
156 assert!(req.matches(&cached));
157 }
158
159 #[test]
160 fn test_matches_project_req_name_none_matches_cached_name() {
161 let req = make_project_scope(None, None);
162 let cached = make_project_scope(Some("proj-1"), Some("myproj"));
163 assert!(req.matches(&cached));
164 }
165
166 #[test]
167 fn test_matches_project_name_mismatch_when_req_name_some() {
168 let req = make_project_scope(None, Some("req-name"));
169 let cached = make_project_scope(Some("proj-1"), Some("cached-name"));
170 assert!(!req.matches(&cached));
171 }
172
173 #[test]
174 fn test_matches_project_both_none() {
175 let req = make_project_scope(None, None);
176 let cached = make_project_scope(None, None);
177 assert!(req.matches(&cached));
178 }
179
180 #[test]
182 fn test_matches_domain_exact_id() {
183 let req = make_domain_scope(Some("d1"), None);
184 let cached = make_domain_scope(Some("d1"), None);
185 assert!(req.matches(&cached));
186 }
187
188 #[test]
189 fn test_matches_domain_id_mismatch() {
190 let req = make_domain_scope(Some("d1"), None);
191 let cached = make_domain_scope(Some("d2"), None);
192 assert!(!req.matches(&cached));
193 }
194
195 #[test]
196 fn test_matches_domain_req_id_none_matches_cached() {
197 let req = make_domain_scope(None, Some("D"));
198 let cached = make_domain_scope(Some("d1"), Some("D"));
199 assert!(req.matches(&cached));
200 }
201
202 #[test]
203 fn test_matches_domain_name_mismatch_when_req_name_some() {
204 let req = make_domain_scope(None, Some("req-D"));
205 let cached = make_domain_scope(Some("d1"), Some("cached-D"));
206 assert!(!req.matches(&cached));
207 }
208
209 #[test]
211 fn test_matches_project_does_not_match_domain() {
212 let req = make_project_scope(Some("p1"), None);
213 let cached = make_domain_scope(Some("p1"), None);
214 assert!(!req.matches(&cached));
215 }
216
217 #[test]
218 fn test_matches_domain_does_not_match_project() {
219 let req = make_domain_scope(Some("d1"), None);
220 let cached = make_project_scope(Some("d1"), None);
221 assert!(!req.matches(&cached));
222 }
223
224 #[test]
225 fn test_matches_unscoped_matches_unscoped() {
226 let req = AuthTokenScope::Unscoped;
227 let cached = AuthTokenScope::Unscoped;
228 assert!(req.matches(&cached));
229 }
230
231 #[test]
232 fn test_matches_unscoped_does_not_match_project() {
233 let req = AuthTokenScope::Unscoped;
234 let cached = make_project_scope(Some("p1"), None);
235 assert!(!req.matches(&cached));
236 }
237
238 #[test]
240 fn test_matches_project_with_domain_exact() {
241 let req = AuthTokenScope::Project(Project {
242 id: Some("project-id".to_string()),
243 name: None,
244 domain: Some(Domain {
245 id: Some("domain-id".to_string()),
246 name: None,
247 }),
248 });
249 let cached = AuthTokenScope::Project(Project {
250 id: Some("project-id".to_string()),
251 name: None,
252 domain: Some(Domain {
253 id: Some("domain-id".to_string()),
254 name: Some("D".to_string()),
255 }),
256 });
257 assert!(req.matches(&cached));
258 }
259
260 #[test]
261 fn test_matches_project_with_domain_mismatch() {
262 let req = AuthTokenScope::Project(Project {
263 id: Some("project-id".to_string()),
264 name: None,
265 domain: Some(Domain {
266 id: Some("domain-1".to_string()),
267 name: None,
268 }),
269 });
270 let cached = AuthTokenScope::Project(Project {
271 id: Some("project-id".to_string()),
272 name: None,
273 domain: Some(Domain {
274 id: Some("domain-2".to_string()),
275 name: Some("D".to_string()),
276 }),
277 });
278 assert!(!req.matches(&cached));
279 }
280
281 #[test]
282 fn test_matches_project_cached_has_domain_req_has_no_domain() {
283 let req = AuthTokenScope::Project(Project {
284 id: Some("project-id".to_string()),
285 name: None,
286 domain: None,
287 });
288 let cached = AuthTokenScope::Project(Project {
289 id: Some("project-id".to_string()),
290 name: None,
291 domain: Some(Domain {
292 id: Some("domain-id".to_string()),
293 name: None,
294 }),
295 });
296 assert!(req.matches(&cached));
297 }
298
299 #[test]
300 fn test_matches_project_req_domain_by_name() {
301 let req = AuthTokenScope::Project(Project {
302 id: None,
303 name: Some("myproj".to_string()),
304 domain: Some(Domain {
305 id: None,
306 name: Some("Default".to_string()),
307 }),
308 });
309 let cached = AuthTokenScope::Project(Project {
310 id: Some("proj-123".to_string()),
311 name: Some("myproj".to_string()),
312 domain: Some(Domain {
313 id: Some("d1".to_string()),
314 name: Some("Default".to_string()),
315 }),
316 });
317 assert!(req.matches(&cached));
318 }
319}