1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct UserEntry {
17 pub id: u64,
19 pub username: String,
21 #[serde(skip_serializing_if = "String::is_empty", default)]
24 pub password_hash: String,
25 #[serde(default)]
27 pub roles: Vec<String>,
28 #[serde(default)]
30 pub is_admin: bool,
31 pub created_epoch: u64,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RoleEntry {
38 pub name: String,
40 #[serde(default)]
42 pub permissions: Vec<Permission>,
43 pub created_epoch: u64,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(tag = "kind", rename_all = "snake_case")]
50pub enum Permission {
51 All,
53 Select { table: String },
55 Insert { table: String },
57 Update { table: String },
59 Delete { table: String },
61 Ddl,
63 Admin,
65}
66
67impl Permission {
68 pub fn satisfies(&self, required: &Permission) -> bool {
76 match (self, required) {
77 (Permission::All, Permission::Admin) => false,
79 (Permission::All, _) => true,
80 (Permission::Admin, Permission::Admin) => true,
81 (Permission::Ddl, Permission::Ddl) => true,
82 (Permission::Select { table: a }, Permission::Select { table: b }) => a == b,
83 (Permission::Insert { table: a }, Permission::Insert { table: b }) => a == b,
84 (Permission::Update { table: a }, Permission::Update { table: b }) => a == b,
85 (Permission::Delete { table: a }, Permission::Delete { table: b }) => a == b,
86 _ => false,
87 }
88 }
89}
90
91impl std::fmt::Display for Permission {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Permission::All => write!(f, "ALL"),
95 Permission::Admin => write!(f, "ADMIN"),
96 Permission::Ddl => write!(f, "DDL"),
97 Permission::Select { table } => write!(f, "SELECT ON {table}"),
98 Permission::Insert { table } => write!(f, "INSERT ON {table}"),
99 Permission::Update { table } => write!(f, "UPDATE ON {table}"),
100 Permission::Delete { table } => write!(f, "DELETE ON {table}"),
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
108pub struct Principal {
109 pub username: String,
110 pub is_admin: bool,
111 pub roles: Vec<String>,
112 pub permissions: Vec<Permission>,
114}
115
116impl Principal {
117 pub fn has_permission(&self, required: &Permission) -> bool {
119 if self.is_admin {
120 return true;
121 }
122 self.permissions.iter().any(|p| p.satisfies(required))
123 }
124}
125
126pub fn hash_password(password: &str) -> Result<String, String> {
134 use argon2::{
135 password_hash::{PasswordHasher, SaltString},
136 Algorithm, Argon2, Version,
137 };
138 use getrandom::getrandom;
139 let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
141 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
142 let mut salt_bytes = [0u8; 32];
144 getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
145 let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
146 let hash = argon2
147 .hash_password(password.as_bytes(), &salt)
148 .map_err(|e| e.to_string())?;
149 Ok(hash.to_string())
150}
151
152pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
155 use argon2::{password_hash::PasswordVerifier, Argon2};
156 let parsed_hash =
157 argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
158 Ok(Argon2::default()
159 .verify_password(password.as_bytes(), &parsed_hash)
160 .is_ok())
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn password_hash_round_trip() {
169 let password = "correct horse battery staple";
170 let hash = hash_password(password).unwrap();
171 assert!(verify_password(password, &hash).unwrap());
172 assert!(!verify_password("wrong password", &hash).unwrap());
173 }
174
175 #[test]
176 fn permission_satisfies() {
177 assert!(Permission::All.satisfies(&Permission::Ddl));
179 assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
180 assert!(Permission::All.satisfies(&Permission::Insert { table: "t".into() }));
181 assert!(!Permission::All.satisfies(&Permission::Admin));
183 assert!(Permission::Select { table: "t".into() }
185 .satisfies(&Permission::Select { table: "t".into() }));
186 assert!(
187 !Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
188 table: "other".into()
189 })
190 );
191 assert!(!Permission::Select { table: "t".into() }
193 .satisfies(&Permission::Insert { table: "t".into() }));
194 assert!(!Permission::Ddl.satisfies(&Permission::Admin));
196 }
197
198 #[test]
199 fn principal_admin_bypasses_checks() {
200 let principal = Principal {
201 username: "admin".into(),
202 is_admin: true,
203 roles: vec![],
204 permissions: vec![],
205 };
206 assert!(principal.has_permission(&Permission::Admin));
207 assert!(principal.has_permission(&Permission::Select {
208 table: "anything".into()
209 }));
210 }
211}