Skip to main content

powdb_auth/
role.rs

1//! Permission and role model for PowDB RBAC.
2//!
3//! A small, fixed permission lattice plus three builtin roles
4//! (`admin`, `readwrite`, `readonly`). This slice does not enforce
5//! permissions anywhere — it only defines the data model.
6
7use std::collections::BTreeSet;
8
9use serde::{Deserialize, Serialize};
10
11/// A single capability a role may grant.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub enum Permission {
14    /// Read rows.
15    Read,
16    /// Insert/update/delete rows.
17    Write,
18    /// Data-definition (create/alter/drop table or index).
19    Ddl,
20    /// Administrative operations (user/role management, etc.).
21    Admin,
22}
23
24/// A named bundle of permissions.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Role {
27    /// Role name (e.g. `admin`).
28    pub name: String,
29    /// Permissions granted by this role.
30    pub permissions: BTreeSet<Permission>,
31}
32
33impl Role {
34    /// Whether this role grants permission `p`.
35    pub fn allows(&self, p: Permission) -> bool {
36        self.permissions.contains(&p)
37    }
38
39    /// Builtin `admin` role: all permissions.
40    pub fn admin() -> Role {
41        Role {
42            name: "admin".to_string(),
43            permissions: BTreeSet::from([
44                Permission::Read,
45                Permission::Write,
46                Permission::Ddl,
47                Permission::Admin,
48            ]),
49        }
50    }
51
52    /// Builtin `readwrite` role: read + write.
53    pub fn readwrite() -> Role {
54        Role {
55            name: "readwrite".to_string(),
56            permissions: BTreeSet::from([Permission::Read, Permission::Write]),
57        }
58    }
59
60    /// Builtin `readonly` role: read only.
61    pub fn readonly() -> Role {
62        Role {
63            name: "readonly".to_string(),
64            permissions: BTreeSet::from([Permission::Read]),
65        }
66    }
67
68    /// Resolve a builtin role by name, or `None` if it is not a builtin.
69    pub fn builtin(name: &str) -> Option<Role> {
70        match name {
71            "admin" => Some(Role::admin()),
72            "readwrite" => Some(Role::readwrite()),
73            "readonly" => Some(Role::readonly()),
74            _ => None,
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn admin_has_all_permissions() {
85        let r = Role::admin();
86        assert_eq!(
87            r.permissions,
88            BTreeSet::from([
89                Permission::Read,
90                Permission::Write,
91                Permission::Ddl,
92                Permission::Admin,
93            ])
94        );
95    }
96
97    #[test]
98    fn admin_allows_everything() {
99        let r = Role::admin();
100        for p in [
101            Permission::Read,
102            Permission::Write,
103            Permission::Ddl,
104            Permission::Admin,
105        ] {
106            assert!(r.allows(p));
107        }
108    }
109
110    #[test]
111    fn readwrite_has_read_and_write_only() {
112        let r = Role::readwrite();
113        assert_eq!(
114            r.permissions,
115            BTreeSet::from([Permission::Read, Permission::Write])
116        );
117        assert!(r.allows(Permission::Read));
118        assert!(r.allows(Permission::Write));
119        assert!(!r.allows(Permission::Ddl));
120        assert!(!r.allows(Permission::Admin));
121    }
122
123    #[test]
124    fn readonly_has_read_only() {
125        let r = Role::readonly();
126        assert_eq!(r.permissions, BTreeSet::from([Permission::Read]));
127        assert!(r.allows(Permission::Read));
128        assert!(!r.allows(Permission::Write));
129        assert!(!r.allows(Permission::Ddl));
130        assert!(!r.allows(Permission::Admin));
131    }
132
133    #[test]
134    fn builtin_resolves_known_roles() {
135        assert_eq!(Role::builtin("admin").unwrap().name, "admin");
136        assert_eq!(Role::builtin("readwrite").unwrap().name, "readwrite");
137        assert_eq!(Role::builtin("readonly").unwrap().name, "readonly");
138    }
139
140    #[test]
141    fn builtin_unknown_is_none() {
142        assert!(Role::builtin("nope").is_none());
143    }
144}