1use std::collections::HashSet;
35use std::fmt;
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct Grant {
44 pub graph: String,
46 pub role: Role,
48}
49
50impl Grant {
51 #[must_use]
53 pub fn new(graph: impl Into<String>, role: Role) -> Self {
54 Self {
55 graph: graph.into(),
56 role,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
67pub struct Identity {
68 user_id: String,
70 roles: HashSet<Role>,
72 grants: Vec<Grant>,
75}
76
77impl Identity {
78 #[must_use]
80 pub fn new(user_id: impl Into<String>, roles: impl IntoIterator<Item = Role>) -> Self {
81 Self {
82 user_id: user_id.into(),
83 roles: roles.into_iter().collect(),
84 grants: Vec::new(),
85 }
86 }
87
88 #[must_use]
93 pub fn anonymous() -> Self {
94 Self {
95 user_id: "anonymous".to_owned(),
96 roles: [Role::Admin].into_iter().collect(),
97 grants: Vec::new(),
98 }
99 }
100
101 #[must_use]
107 pub fn with_grants(mut self, grants: impl IntoIterator<Item = Grant>) -> Self {
108 self.grants = grants.into_iter().collect();
109 self
110 }
111
112 #[must_use]
114 pub fn user_id(&self) -> &str {
115 &self.user_id
116 }
117
118 #[must_use]
120 pub fn roles(&self) -> &HashSet<Role> {
121 &self.roles
122 }
123
124 #[must_use]
126 pub fn has_role(&self, role: Role) -> bool {
127 self.roles.contains(&role)
128 }
129
130 #[must_use]
134 pub fn can_read(&self) -> bool {
135 !self.roles.is_empty()
136 }
137
138 #[must_use]
141 pub fn can_write(&self) -> bool {
142 self.has_role(Role::Admin) || self.has_role(Role::ReadWrite)
143 }
144
145 #[must_use]
148 pub fn can_admin(&self) -> bool {
149 self.has_role(Role::Admin)
150 }
151
152 #[must_use]
154 pub fn grants(&self) -> &[Grant] {
155 &self.grants
156 }
157
158 #[must_use]
160 pub fn has_grants(&self) -> bool {
161 !self.grants.is_empty()
162 }
163
164 #[must_use]
171 pub fn can_access_graph(&self, graph: &str, required: Role) -> bool {
172 if self.grants.is_empty() {
173 return match required {
175 Role::ReadOnly => self.can_read(),
176 Role::ReadWrite => self.can_write(),
177 Role::Admin => self.can_admin(),
178 };
179 }
180 self.grants.iter().any(|g| {
182 g.graph.eq_ignore_ascii_case(graph)
183 && match required {
184 Role::ReadOnly => true, Role::ReadWrite => g.role == Role::ReadWrite || g.role == Role::Admin,
186 Role::Admin => g.role == Role::Admin,
187 }
188 })
189 }
190}
191
192impl fmt::Display for Identity {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "{}", self.user_id)
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
205pub enum Role {
206 Admin,
209 ReadWrite,
212 ReadOnly,
215}
216
217impl fmt::Display for Role {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match self {
220 Self::Admin => write!(f, "Admin"),
221 Self::ReadWrite => write!(f, "ReadWrite"),
222 Self::ReadOnly => write!(f, "ReadOnly"),
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum StatementKind {
234 Read,
236 Write,
238 Admin,
241 Transaction,
244}
245
246impl StatementKind {
247 #[must_use]
251 pub fn required_role(self) -> Option<Role> {
252 match self {
253 Self::Read => Some(Role::ReadOnly),
254 Self::Write => Some(Role::ReadWrite),
255 Self::Admin => Some(Role::Admin),
256 Self::Transaction => None,
257 }
258 }
259}
260
261impl fmt::Display for StatementKind {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 match self {
264 Self::Read => write!(f, "read"),
265 Self::Write => write!(f, "write"),
266 Self::Admin => write!(f, "admin"),
267 Self::Transaction => write!(f, "transaction control"),
268 }
269 }
270}
271
272pub(crate) fn check_permission(
278 identity: &Identity,
279 kind: StatementKind,
280) -> std::result::Result<(), PermissionDenied> {
281 match kind {
282 StatementKind::Transaction => Ok(()),
283 StatementKind::Read => {
284 if identity.can_read() {
285 Ok(())
286 } else {
287 Err(PermissionDenied {
288 operation: kind,
289 required: Role::ReadOnly,
290 user_id: identity.user_id.clone(),
291 })
292 }
293 }
294 StatementKind::Write => {
295 if identity.can_write() {
296 Ok(())
297 } else {
298 Err(PermissionDenied {
299 operation: kind,
300 required: Role::ReadWrite,
301 user_id: identity.user_id.clone(),
302 })
303 }
304 }
305 StatementKind::Admin => {
306 if identity.can_admin() {
307 Ok(())
308 } else {
309 Err(PermissionDenied {
310 operation: kind,
311 required: Role::Admin,
312 user_id: identity.user_id.clone(),
313 })
314 }
315 }
316 }
317}
318
319#[derive(Debug, Clone)]
321pub struct PermissionDenied {
322 pub operation: StatementKind,
324 pub required: Role,
326 pub user_id: String,
328}
329
330impl fmt::Display for PermissionDenied {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 write!(
333 f,
334 "permission denied: {} operations require {} role (user: {})",
335 self.operation, self.required, self.user_id
336 )
337 }
338}
339
340impl std::error::Error for PermissionDenied {}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn anonymous_has_admin_role() {
348 let id = Identity::anonymous();
349 assert_eq!(id.user_id(), "anonymous");
350 assert!(id.has_role(Role::Admin));
351 assert!(id.can_read());
352 assert!(id.can_write());
353 assert!(id.can_admin());
354 }
355
356 #[test]
357 fn read_only_identity() {
358 let id = Identity::new("reader", [Role::ReadOnly]);
359 assert!(id.can_read());
360 assert!(!id.can_write());
361 assert!(!id.can_admin());
362 }
363
364 #[test]
365 fn read_write_identity() {
366 let id = Identity::new("writer", [Role::ReadWrite]);
367 assert!(id.can_read());
368 assert!(id.can_write());
369 assert!(!id.can_admin());
370 }
371
372 #[test]
373 fn admin_identity() {
374 let id = Identity::new("admin", [Role::Admin]);
375 assert!(id.can_read());
376 assert!(id.can_write());
377 assert!(id.can_admin());
378 }
379
380 #[test]
381 fn empty_roles_cannot_read() {
382 let id = Identity::new("nobody", std::iter::empty::<Role>());
383 assert!(!id.can_read());
384 assert!(!id.can_write());
385 assert!(!id.can_admin());
386 }
387
388 #[test]
389 fn role_display() {
390 assert_eq!(Role::Admin.to_string(), "Admin");
391 assert_eq!(Role::ReadWrite.to_string(), "ReadWrite");
392 assert_eq!(Role::ReadOnly.to_string(), "ReadOnly");
393 }
394
395 #[test]
396 fn statement_kind_required_role() {
397 assert_eq!(StatementKind::Read.required_role(), Some(Role::ReadOnly));
398 assert_eq!(StatementKind::Write.required_role(), Some(Role::ReadWrite));
399 assert_eq!(StatementKind::Admin.required_role(), Some(Role::Admin));
400 assert_eq!(StatementKind::Transaction.required_role(), None);
401 }
402
403 #[test]
404 fn check_permission_allows_transaction_for_all() {
405 let readonly = Identity::new("r", [Role::ReadOnly]);
406 assert!(check_permission(&readonly, StatementKind::Transaction).is_ok());
407
408 let nobody = Identity::new("n", std::iter::empty::<Role>());
409 assert!(check_permission(&nobody, StatementKind::Transaction).is_ok());
410 }
411
412 #[test]
413 fn check_permission_denies_write_for_readonly() {
414 let id = Identity::new("reader", [Role::ReadOnly]);
415 let err = check_permission(&id, StatementKind::Write).unwrap_err();
416 assert_eq!(err.required, Role::ReadWrite);
417 assert_eq!(err.operation, StatementKind::Write);
418 assert!(err.to_string().contains("permission denied"));
419 }
420
421 #[test]
422 fn check_permission_denies_admin_for_readwrite() {
423 let id = Identity::new("writer", [Role::ReadWrite]);
424 let err = check_permission(&id, StatementKind::Admin).unwrap_err();
425 assert_eq!(err.required, Role::Admin);
426 }
427
428 #[test]
429 fn identity_display() {
430 let id = Identity::new("app-service", [Role::ReadWrite]);
431 assert_eq!(id.to_string(), "app-service");
432 }
433
434 #[test]
435 fn identity_with_multiple_roles() {
436 let id = Identity::new("alix", [Role::ReadOnly, Role::ReadWrite]);
437 assert!(id.can_read());
438 assert!(id.can_write());
439 assert!(!id.can_admin());
440 assert!(id.has_role(Role::ReadOnly));
441 assert!(id.has_role(Role::ReadWrite));
442 assert!(!id.has_role(Role::Admin));
443 assert_eq!(id.roles().len(), 2);
444 }
445
446 #[test]
447 fn identity_with_all_roles() {
448 let id = Identity::new("gus", [Role::ReadOnly, Role::ReadWrite, Role::Admin]);
449 assert!(id.can_read());
450 assert!(id.can_write());
451 assert!(id.can_admin());
452 assert_eq!(id.roles().len(), 3);
453 }
454
455 #[test]
456 fn permission_denied_error_message_contains_user_and_role() {
457 let id = Identity::new("alix", [Role::ReadOnly]);
458 let err = check_permission(&id, StatementKind::Write).unwrap_err();
459 let msg = err.to_string();
460 assert!(msg.contains("alix"), "error should contain user id");
461 assert!(
462 msg.contains("ReadWrite"),
463 "error should contain required role"
464 );
465 assert!(msg.contains("write"), "error should contain operation kind");
466 }
467
468 #[test]
469 fn permission_denied_admin_error_message() {
470 let id = Identity::new("gus", [Role::ReadOnly]);
471 let err = check_permission(&id, StatementKind::Admin).unwrap_err();
472 let msg = err.to_string();
473 assert!(msg.contains("gus"));
474 assert!(msg.contains("Admin"));
475 assert!(msg.contains("admin"));
476 }
477
478 #[test]
479 fn check_permission_denies_read_for_no_roles() {
480 let id = Identity::new("nobody", std::iter::empty::<Role>());
481 let err = check_permission(&id, StatementKind::Read).unwrap_err();
482 assert_eq!(err.required, Role::ReadOnly);
483 assert_eq!(err.operation, StatementKind::Read);
484 assert!(err.to_string().contains("nobody"));
485 }
486
487 #[test]
488 fn check_permission_allows_read_for_readonly() {
489 let id = Identity::new("alix", [Role::ReadOnly]);
490 assert!(check_permission(&id, StatementKind::Read).is_ok());
491 }
492
493 #[test]
494 fn check_permission_allows_admin_for_admin() {
495 let id = Identity::new("gus", [Role::Admin]);
496 assert!(check_permission(&id, StatementKind::Admin).is_ok());
497 }
498
499 #[test]
500 fn check_permission_allows_write_for_readwrite() {
501 let id = Identity::new("alix", [Role::ReadWrite]);
502 assert!(check_permission(&id, StatementKind::Write).is_ok());
503 }
504
505 #[test]
506 fn statement_kind_display() {
507 assert_eq!(StatementKind::Read.to_string(), "read");
508 assert_eq!(StatementKind::Write.to_string(), "write");
509 assert_eq!(StatementKind::Admin.to_string(), "admin");
510 assert_eq!(
511 StatementKind::Transaction.to_string(),
512 "transaction control"
513 );
514 }
515
516 #[test]
517 fn permission_denied_is_std_error() {
518 let id = Identity::new("alix", [Role::ReadOnly]);
519 let err = check_permission(&id, StatementKind::Write).unwrap_err();
520 let _: &dyn std::error::Error = &err;
522 }
523
524 #[test]
527 fn no_grants_means_unrestricted() {
528 let id = Identity::new("alix", [Role::ReadWrite]);
529 assert!(id.can_access_graph("any_graph", Role::ReadWrite));
530 assert!(id.can_access_graph("other", Role::ReadOnly));
531 assert!(!id.has_grants());
532 }
533
534 #[test]
535 fn grant_restricts_to_listed_graphs() {
536 let id = Identity::new("gus", [Role::ReadWrite]).with_grants([
537 Grant::new("social", Role::ReadWrite),
538 Grant::new("analytics", Role::ReadOnly),
539 ]);
540 assert!(id.has_grants());
541 assert!(id.can_access_graph("social", Role::ReadWrite));
542 assert!(id.can_access_graph("social", Role::ReadOnly));
543 assert!(id.can_access_graph("analytics", Role::ReadOnly));
544 assert!(!id.can_access_graph("analytics", Role::ReadWrite));
545 assert!(!id.can_access_graph("secret", Role::ReadOnly));
546 }
547
548 #[test]
549 fn grant_admin_implies_all() {
550 let id =
551 Identity::new("admin", [Role::Admin]).with_grants([Grant::new("prod", Role::Admin)]);
552 assert!(id.can_access_graph("prod", Role::Admin));
553 assert!(id.can_access_graph("prod", Role::ReadWrite));
554 assert!(id.can_access_graph("prod", Role::ReadOnly));
555 }
556
557 #[test]
558 fn grant_case_insensitive() {
559 let id = Identity::new("alix", [Role::ReadWrite])
560 .with_grants([Grant::new("Social", Role::ReadWrite)]);
561 assert!(id.can_access_graph("social", Role::ReadOnly));
562 assert!(id.can_access_graph("SOCIAL", Role::ReadWrite));
563 }
564
565 #[test]
566 fn grant_display() {
567 let g = Grant::new("social", Role::ReadWrite);
568 assert_eq!(g.graph, "social");
569 assert_eq!(g.role, Role::ReadWrite);
570 }
571}