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 username: String,
155 pub is_admin: bool,
156 pub roles: Vec<String>,
157 pub permissions: Vec<Permission>,
159}
160
161impl Principal {
162 pub fn has_permission(&self, required: &Permission) -> bool {
164 if self.is_admin {
165 return true;
166 }
167 self.permissions.iter().any(|p| p.satisfies(required))
168 }
169
170 pub fn column_access(&self, table: &str, operation: ColumnOperation) -> ColumnAccess {
171 if self.is_admin
172 || self
173 .permissions
174 .iter()
175 .any(|permission| matches!(permission, Permission::All))
176 {
177 return ColumnAccess::All;
178 }
179 let full = self
180 .permissions
181 .iter()
182 .any(|permission| match (operation, permission) {
183 (ColumnOperation::Select, Permission::Select { table: granted })
184 | (ColumnOperation::Insert, Permission::Insert { table: granted })
185 | (ColumnOperation::Update, Permission::Update { table: granted }) => {
186 granted == table
187 }
188 _ => false,
189 });
190 if full {
191 return ColumnAccess::All;
192 }
193 let mut columns = Vec::new();
194 for permission in &self.permissions {
195 let grant = match (operation, permission) {
196 (
197 ColumnOperation::Select,
198 Permission::SelectColumns {
199 table: granted,
200 columns,
201 },
202 )
203 | (
204 ColumnOperation::Insert,
205 Permission::InsertColumns {
206 table: granted,
207 columns,
208 },
209 )
210 | (
211 ColumnOperation::Update,
212 Permission::UpdateColumns {
213 table: granted,
214 columns,
215 },
216 ) if granted == table => Some(columns),
217 _ => None,
218 };
219 if let Some(grant) = grant {
220 for column in grant {
221 if !columns.contains(column) {
222 columns.push(column.clone());
223 }
224 }
225 }
226 }
227 if columns.is_empty() {
228 ColumnAccess::Denied
229 } else {
230 ColumnAccess::Columns(columns)
231 }
232 }
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum ColumnOperation {
237 Select,
238 Insert,
239 Update,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum ColumnAccess {
244 All,
245 Columns(Vec<String>),
246 Denied,
247}
248
249pub fn hash_password(password: &str) -> Result<String, String> {
257 use argon2::{
258 password_hash::{PasswordHasher, SaltString},
259 Algorithm, Argon2, Version,
260 };
261 use getrandom::getrandom;
262 let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
264 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
265 let mut salt_bytes = [0u8; 32];
267 getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
268 let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
269 let hash = argon2
270 .hash_password(password.as_bytes(), &salt)
271 .map_err(|e| e.to_string())?;
272 Ok(hash.to_string())
273}
274
275pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
278 use argon2::{password_hash::PasswordVerifier, Argon2};
279 let parsed_hash =
280 argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
281 Ok(Argon2::default()
282 .verify_password(password.as_bytes(), &parsed_hash)
283 .is_ok())
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn password_hash_round_trip() {
292 let password = "correct horse battery staple";
293 let hash = hash_password(password).unwrap();
294 assert!(verify_password(password, &hash).unwrap());
295 assert!(!verify_password("wrong password", &hash).unwrap());
296 }
297
298 #[test]
299 fn permission_satisfies() {
300 assert!(Permission::All.satisfies(&Permission::Ddl));
302 assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
303 assert!(Permission::All.satisfies(&Permission::Insert { table: "t".into() }));
304 assert!(!Permission::All.satisfies(&Permission::Admin));
306 assert!(Permission::Select { table: "t".into() }
308 .satisfies(&Permission::Select { table: "t".into() }));
309 assert!(
310 !Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
311 table: "other".into()
312 })
313 );
314 assert!(!Permission::Select { table: "t".into() }
316 .satisfies(&Permission::Insert { table: "t".into() }));
317 assert!(!Permission::Ddl.satisfies(&Permission::Admin));
319 }
320
321 #[test]
322 fn principal_admin_bypasses_checks() {
323 let principal = Principal {
324 username: "admin".into(),
325 is_admin: true,
326 roles: vec![],
327 permissions: vec![],
328 };
329 assert!(principal.has_permission(&Permission::Admin));
330 assert!(principal.has_permission(&Permission::Select {
331 table: "anything".into()
332 }));
333 }
334}