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