use super::{CertIdentity, OAuthIdentity, Role};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthSource {
Password,
ClientCert,
Oauth,
}
#[derive(Debug, Clone)]
pub enum AuthResult {
Authenticated {
username: String,
role: Role,
source: AuthSource,
},
Anonymous,
Denied(String),
}
impl AuthResult {
pub fn password(username: impl Into<String>, role: Role) -> Self {
Self::Authenticated {
username: username.into(),
role,
source: AuthSource::Password,
}
}
pub fn from_cert(id: CertIdentity) -> Self {
Self::Authenticated {
username: id.username,
role: id.role,
source: AuthSource::ClientCert,
}
}
pub fn from_oauth(id: OAuthIdentity) -> Self {
Self::Authenticated {
username: id.username,
role: id.role,
source: AuthSource::Oauth,
}
}
pub fn summary(&self) -> String {
match self {
Self::Authenticated {
username,
role,
source,
} => {
let src = match source {
AuthSource::Password => "pwd",
AuthSource::ClientCert => "cert",
AuthSource::Oauth => "oauth",
};
format!("user={username} role={role} via={src}")
}
Self::Anonymous => "anonymous".to_string(),
Self::Denied(reason) => format!("denied: {reason}"),
}
}
pub fn is_authenticated(&self) -> bool {
matches!(self, Self::Authenticated { .. })
}
}
pub fn check_permission(
auth: &AuthResult,
requires_write: bool,
requires_admin: bool,
) -> Result<(), String> {
match auth {
AuthResult::Authenticated { role, .. } => {
if requires_admin && !role.can_admin() {
return Err("admin role required".into());
}
if requires_write && !role.can_write() {
return Err("write permission required".into());
}
Ok(())
}
AuthResult::Anonymous => {
if requires_admin {
return Err("admin authentication required".into());
}
Ok(())
}
AuthResult::Denied(reason) => Err(reason.clone()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_admin_can_do_everything() {
let auth = AuthResult::Authenticated {
username: "root".into(),
role: Role::Admin,
source: AuthSource::Password,
};
assert!(check_permission(&auth, false, false).is_ok());
assert!(check_permission(&auth, true, false).is_ok());
assert!(check_permission(&auth, false, true).is_ok());
assert!(check_permission(&auth, true, true).is_ok());
}
#[test]
fn test_write_role_cannot_admin() {
let auth = AuthResult::Authenticated {
username: "writer".into(),
role: Role::Write,
source: AuthSource::Password,
};
assert!(check_permission(&auth, false, false).is_ok());
assert!(check_permission(&auth, true, false).is_ok());
assert!(check_permission(&auth, false, true).is_err());
}
#[test]
fn test_read_role_cannot_write() {
let auth = AuthResult::Authenticated {
username: "reader".into(),
role: Role::Read,
source: AuthSource::Password,
};
assert!(check_permission(&auth, false, false).is_ok());
assert!(check_permission(&auth, true, false).is_err());
assert!(check_permission(&auth, false, true).is_err());
}
#[test]
fn test_anonymous_access() {
let auth = AuthResult::Anonymous;
assert!(check_permission(&auth, false, false).is_ok());
assert!(check_permission(&auth, true, false).is_ok());
assert!(check_permission(&auth, false, true).is_err());
}
#[test]
fn test_denied_always_fails() {
let auth = AuthResult::Denied("bad token".into());
assert!(check_permission(&auth, false, false).is_err());
assert!(check_permission(&auth, true, true).is_err());
}
#[test]
fn test_auth_result_summary() {
let auth = AuthResult::Authenticated {
username: "alice".into(),
role: Role::Admin,
source: AuthSource::Password,
};
assert!(auth.summary().contains("alice"));
assert!(auth.is_authenticated());
let anon = AuthResult::Anonymous;
assert_eq!(anon.summary(), "anonymous");
assert!(!anon.is_authenticated());
}
}