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 {
72 match (self, required) {
73 (Permission::All, _) => true,
74 (Permission::Admin, Permission::Admin) => true,
75 (Permission::Ddl, Permission::Ddl) => true,
76 (Permission::Select { table: a }, Permission::Select { table: b }) => a == b,
77 (Permission::Insert { table: a }, Permission::Insert { table: b }) => a == b,
78 (Permission::Update { table: a }, Permission::Update { table: b }) => a == b,
79 (Permission::Delete { table: a }, Permission::Delete { table: b }) => a == b,
80 _ => false,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
88pub struct Principal {
89 pub username: String,
90 pub is_admin: bool,
91 pub roles: Vec<String>,
92 pub permissions: Vec<Permission>,
94}
95
96impl Principal {
97 pub fn has_permission(&self, required: &Permission) -> bool {
99 if self.is_admin {
100 return true;
101 }
102 self.permissions.iter().any(|p| p.satisfies(required))
103 }
104}
105
106pub fn hash_password(password: &str) -> Result<String, String> {
114 use argon2::{
115 password_hash::{PasswordHasher, SaltString},
116 Algorithm, Argon2, Version,
117 };
118 use getrandom::getrandom;
119 let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
121 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
122 let mut salt_bytes = [0u8; 32];
124 getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
125 let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
126 let hash = argon2
127 .hash_password(password.as_bytes(), &salt)
128 .map_err(|e| e.to_string())?;
129 Ok(hash.to_string())
130}
131
132pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
135 use argon2::{password_hash::PasswordVerifier, Argon2};
136 let parsed_hash =
137 argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
138 Ok(Argon2::default()
139 .verify_password(password.as_bytes(), &parsed_hash)
140 .is_ok())
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn password_hash_round_trip() {
149 let password = "correct horse battery staple";
150 let hash = hash_password(password).unwrap();
151 assert!(verify_password(password, &hash).unwrap());
152 assert!(!verify_password("wrong password", &hash).unwrap());
153 }
154
155 #[test]
156 fn permission_satisfies() {
157 assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
158 assert!(Permission::Select { table: "t".into() }
159 .satisfies(&Permission::Select { table: "t".into() }));
160 assert!(
161 !Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
162 table: "other".into()
163 })
164 );
165 assert!(!Permission::Select { table: "t".into() }
166 .satisfies(&Permission::Insert { table: "t".into() }));
167 }
168
169 #[test]
170 fn principal_admin_bypasses_checks() {
171 let principal = Principal {
172 username: "admin".into(),
173 is_admin: true,
174 roles: vec![],
175 permissions: vec![],
176 };
177 assert!(principal.has_permission(&Permission::Admin));
178 assert!(principal.has_permission(&Permission::Select {
179 table: "anything".into()
180 }));
181 }
182}