use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserEntry {
pub id: u64,
pub username: String,
#[serde(skip_serializing_if = "String::is_empty", default)]
pub password_hash: String,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub is_admin: bool,
pub created_epoch: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleEntry {
pub name: String,
#[serde(default)]
pub permissions: Vec<Permission>,
pub created_epoch: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Permission {
All,
Select { table: String },
Insert { table: String },
Update { table: String },
Delete { table: String },
Ddl,
Admin,
}
impl Permission {
pub fn satisfies(&self, required: &Permission) -> bool {
match (self, required) {
(Permission::All, _) => true,
(Permission::Admin, Permission::Admin) => true,
(Permission::Ddl, Permission::Ddl) => true,
(Permission::Select { table: a }, Permission::Select { table: b }) => a == b,
(Permission::Insert { table: a }, Permission::Insert { table: b }) => a == b,
(Permission::Update { table: a }, Permission::Update { table: b }) => a == b,
(Permission::Delete { table: a }, Permission::Delete { table: b }) => a == b,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct Principal {
pub username: String,
pub is_admin: bool,
pub roles: Vec<String>,
pub permissions: Vec<Permission>,
}
impl Principal {
pub fn has_permission(&self, required: &Permission) -> bool {
if self.is_admin {
return true;
}
self.permissions.iter().any(|p| p.satisfies(required))
}
}
pub fn hash_password(password: &str) -> Result<String, String> {
use argon2::{
password_hash::{PasswordHasher, SaltString},
Algorithm, Argon2, Version,
};
use getrandom::getrandom;
let params = argon2::Params::new(19 * 1024, 2, 1, None).map_err(|e| e.to_string())?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut salt_bytes = [0u8; 32];
getrandom(&mut salt_bytes).map_err(|e| e.to_string())?;
let salt = SaltString::encode_b64(&salt_bytes).map_err(|e| e.to_string())?;
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| e.to_string())?;
Ok(hash.to_string())
}
pub fn verify_password(password: &str, phc_hash: &str) -> Result<bool, String> {
use argon2::{password_hash::PasswordVerifier, Argon2};
let parsed_hash =
argon2::PasswordHash::new(phc_hash).map_err(|e| format!("malformed hash: {e}"))?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn password_hash_round_trip() {
let password = "correct horse battery staple";
let hash = hash_password(password).unwrap();
assert!(verify_password(password, &hash).unwrap());
assert!(!verify_password("wrong password", &hash).unwrap());
}
#[test]
fn permission_satisfies() {
assert!(Permission::All.satisfies(&Permission::Select { table: "t".into() }));
assert!(Permission::Select { table: "t".into() }
.satisfies(&Permission::Select { table: "t".into() }));
assert!(
!Permission::Select { table: "t".into() }.satisfies(&Permission::Select {
table: "other".into()
})
);
assert!(!Permission::Select { table: "t".into() }
.satisfies(&Permission::Insert { table: "t".into() }));
}
#[test]
fn principal_admin_bypasses_checks() {
let principal = Principal {
username: "admin".into(),
is_admin: true,
roles: vec![],
permissions: vec![],
};
assert!(principal.has_permission(&Permission::Admin));
assert!(principal.has_permission(&Permission::Select {
table: "anything".into()
}));
}
}