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 SelectColumns { table: String, columns: Vec<String> },
63 InsertColumns { table: String, columns: Vec<String> },
65 UpdateColumns { table: String, columns: Vec<String> },
67 Ddl,
69 Admin,
71}
72
73impl Permission {
74 pub fn satisfies(&self, required: &Permission) -> bool {
82 match (self, required) {
83 (Permission::All, Permission::Admin) => false,
85 (Permission::All, _) => true,
86 (Permission::Admin, Permission::Admin) => true,
87 (Permission::Ddl, Permission::Ddl) => true,
88 (Permission::Select { table: a }, Permission::Select { table: b }) => a == b,
89 (Permission::Insert { table: a }, Permission::Insert { table: b }) => a == b,
90 (Permission::Update { table: a }, Permission::Update { table: b }) => a == b,
91 (Permission::Delete { table: a }, Permission::Delete { table: b }) => a == b,
92 (
93 Permission::SelectColumns {
94 table: a,
95 columns: granted,
96 },
97 Permission::SelectColumns {
98 table: b,
99 columns: required,
100 },
101 )
102 | (
103 Permission::InsertColumns {
104 table: a,
105 columns: granted,
106 },
107 Permission::InsertColumns {
108 table: b,
109 columns: required,
110 },
111 )
112 | (
113 Permission::UpdateColumns {
114 table: a,
115 columns: granted,
116 },
117 Permission::UpdateColumns {
118 table: b,
119 columns: required,
120 },
121 ) => a == b && required.iter().all(|column| granted.contains(column)),
122 _ => false,
123 }
124 }
125}
126
127impl std::fmt::Display for Permission {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 Permission::All => write!(f, "ALL"),
131 Permission::Admin => write!(f, "ADMIN"),
132 Permission::Ddl => write!(f, "DDL"),
133 Permission::Select { table } => write!(f, "SELECT ON {table}"),
134 Permission::Insert { table } => write!(f, "INSERT ON {table}"),
135 Permission::Update { table } => write!(f, "UPDATE ON {table}"),
136 Permission::Delete { table } => write!(f, "DELETE ON {table}"),
137 Permission::SelectColumns { table, columns } => {
138 write!(f, "SELECT ({}) ON {table}", columns.join(", "))
139 }
140 Permission::InsertColumns { table, columns } => {
141 write!(f, "INSERT ({}) ON {table}", columns.join(", "))
142 }
143 Permission::UpdateColumns { table, columns } => {
144 write!(f, "UPDATE ({}) ON {table}", columns.join(", "))
145 }
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
153pub struct Principal {
154 pub user_id: u64,
156 pub created_epoch: u64,
158 pub username: String,
159 pub is_admin: bool,
160 pub roles: Vec<String>,
161 pub permissions: Vec<Permission>,
163}
164
165impl Principal {
166 pub fn has_permission(&self, required: &Permission) -> bool {
168 if self.is_admin {
169 return true;
170 }
171 self.permissions.iter().any(|p| p.satisfies(required))
172 }
173
174 pub fn column_access(&self, table: &str, operation: ColumnOperation) -> ColumnAccess {
175 if self.is_admin
176 || self
177 .permissions
178 .iter()
179 .any(|permission| matches!(permission, Permission::All))
180 {
181 return ColumnAccess::All;
182 }
183 let full = self
184 .permissions
185 .iter()
186 .any(|permission| match (operation, permission) {
187 (ColumnOperation::Select, Permission::Select { table: granted })
188 | (ColumnOperation::Insert, Permission::Insert { table: granted })
189 | (ColumnOperation::Update, Permission::Update { table: granted }) => {
190 granted == table
191 }
192 _ => false,
193 });
194 if full {
195 return ColumnAccess::All;
196 }
197 let mut columns = Vec::new();
198 for permission in &self.permissions {
199 let grant = match (operation, permission) {
200 (
201 ColumnOperation::Select,
202 Permission::SelectColumns {
203 table: granted,
204 columns,
205 },
206 )
207 | (
208 ColumnOperation::Insert,
209 Permission::InsertColumns {
210 table: granted,
211 columns,
212 },
213 )
214 | (
215 ColumnOperation::Update,
216 Permission::UpdateColumns {
217 table: granted,
218 columns,
219 },
220 ) if granted == table => Some(columns),
221 _ => None,
222 };
223 if let Some(grant) = grant {
224 for column in grant {
225 if !columns.contains(column) {
226 columns.push(column.clone());
227 }
228 }
229 }
230 }
231 if columns.is_empty() {
232 ColumnAccess::Denied
233 } else {
234 ColumnAccess::Columns(columns)
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum ColumnOperation {
241 Select,
242 Insert,
243 Update,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum ColumnAccess {
248 All,
249 Columns(Vec<String>),
250 Denied,
251}
252
253pub fn hash_password(password: &str) -> Result<String, String> {
261 use argon2::{
262 password_hash::{PasswordHasher, SaltString},
263 Algorithm, Argon2, Version,
264 };
265 use getrandom::getrandom;
266 let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
268 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
269 let mut salt_bytes = [0u8; 32];
271 getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
272 let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
273 let hash = argon2
274 .hash_password(password.as_bytes(), &salt)
275 .map_err(|e| e.to_string())?;
276 Ok(hash.to_string())
277}
278
279pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
282 use argon2::{password_hash::PasswordVerifier, Argon2};
283 let parsed_hash =
284 argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
285 Ok(Argon2::default()
286 .verify_password(password.as_bytes(), &parsed_hash)
287 .is_ok())
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn password_hash_round_trip() {
296 let password = "correct horse battery staple";
297 let hash = hash_password(password).unwrap();
298 assert!(verify_password(password, &hash).unwrap());
299 assert!(!verify_password("wrong password", &hash).unwrap());
300 }
301
302 #[test]
303 fn permission_satisfies() {
304 assert!(Permission::All.satisfies(&Permission::Ddl));
306 assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
307 assert!(Permission::All.satisfies(&Permission::Insert { table: "t".into() }));
308 assert!(!Permission::All.satisfies(&Permission::Admin));
310 assert!(Permission::Select { table: "t".into() }
312 .satisfies(&Permission::Select { table: "t".into() }));
313 assert!(
314 !Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
315 table: "other".into()
316 })
317 );
318 assert!(!Permission::Select { table: "t".into() }
320 .satisfies(&Permission::Insert { table: "t".into() }));
321 assert!(!Permission::Ddl.satisfies(&Permission::Admin));
323 }
324
325 #[test]
326 fn principal_admin_bypasses_checks() {
327 let principal = Principal {
328 user_id: 0,
329 created_epoch: 0,
330 username: "admin".into(),
331 is_admin: true,
332 roles: vec![],
333 permissions: vec![],
334 };
335 assert!(principal.has_permission(&Permission::Admin));
336 assert!(principal.has_permission(&Permission::Select {
337 table: "anything".into()
338 }));
339 }
340}