use std::str::FromStr;
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug, PartialEq)]
pub enum PolicyError {
#[error("User account is banned.")]
UserBanned,
#[error("User account is suspended.")]
UserSuspended,
#[error("Insufficient permissions to perform this action.")]
InsufficientPermissions,
#[error("User trust score ({0}) is below the required threshold ({1}) for this action.")]
LowTrustScore(f32, f32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserStatus {
Active,
Suspended,
Banned,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Role {
User,
TrustedUser,
Moderator,
Admin,
}
impl FromStr for Role {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"user" => Ok(Role::User),
"trusted_user" => Ok(Role::TrustedUser),
"moderator" => Ok(Role::Moderator),
"admin" => Ok(Role::Admin),
_ => Err(()),
}
}
}
pub struct PolicyContext<'a> {
pub user_id: Uuid,
pub roles: &'a [Role],
pub status: &'a UserStatus,
pub trust_score: f32,
}
#[derive(Debug, PartialEq)]
pub enum Action<'a> {
ReadOwnData,
UpdateOwnProfile,
ReadDeviceData { device_id: Uuid },
ReadUserData { target_user_id: &'a Uuid },
GenerateSecurityReport,
PerformSensitiveTransaction,
}
pub struct PolicyEngine;
impl PolicyEngine {
pub fn can_execute(context: &PolicyContext, action: &Action) -> Result<(), PolicyError> {
match context.status {
UserStatus::Banned => return Err(PolicyError::UserBanned),
UserStatus::Suspended => return Err(PolicyError::UserSuspended),
UserStatus::Active => (), }
if context.roles.contains(&Role::Admin) {
return Ok(());
}
match action {
Action::PerformSensitiveTransaction => {
let required_score = 0.9;
if context.trust_score < required_score {
return Err(PolicyError::LowTrustScore(context.trust_score, required_score));
}
}
_ => (), }
let has_permission = context.roles.iter().any(|role| {
match action {
Action::ReadOwnData | Action::UpdateOwnProfile => true,
Action::ReadDeviceData { .. } => *role >= Role::Moderator,
Action::ReadUserData { target_user_id } => {
&context.user_id == *target_user_id || *role >= Role::Moderator
}
Action::PerformSensitiveTransaction => *role >= Role::TrustedUser,
Action::GenerateSecurityReport => *role >= Role::Moderator,
}
});
if has_permission {
Ok(())
} else {
Err(PolicyError::InsufficientPermissions)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permissions() {
let active_status = UserStatus::Active;
let user_id = Uuid::new_v4();
let other_user_id = Uuid::new_v4();
let user_context = PolicyContext { user_id, roles: &[Role::User], status: &active_status, trust_score: 0.7 };
let trusted_user_context = PolicyContext { user_id, roles: &[Role::User, Role::TrustedUser], status: &active_status, trust_score: 0.95 };
let moderator_context = PolicyContext { user_id, roles: &[Role::Moderator], status: &active_status, trust_score: 0.8 };
let admin_context = PolicyContext { user_id, roles: &[Role::Admin], status: &active_status, trust_score: 1.0 };
assert_eq!(PolicyEngine::can_execute(&user_context, &Action::ReadOwnData), Ok(()));
assert_eq!(PolicyEngine::can_execute(&user_context, &Action::ReadUserData { target_user_id: &user_id }), Ok(()));
assert_eq!(PolicyEngine::can_execute(&user_context, &Action::ReadUserData { target_user_id: &other_user_id }), Err(PolicyError::InsufficientPermissions));
assert_eq!(PolicyEngine::can_execute(&moderator_context, &Action::ReadUserData { target_user_id: &other_user_id }), Ok(()));
assert_eq!(PolicyEngine::can_execute(&moderator_context, &Action::GenerateSecurityReport), Ok(()));
assert_eq!(PolicyEngine::can_execute(&user_context, &Action::GenerateSecurityReport), Err(PolicyError::InsufficientPermissions));
assert_eq!(PolicyEngine::can_execute(&trusted_user_context, &Action::PerformSensitiveTransaction), Ok(()));
assert_eq!(PolicyEngine::can_execute(&user_context, &Action::PerformSensitiveTransaction), Err(PolicyError::LowTrustScore(0.7, 0.9)));
assert_eq!(PolicyEngine::can_execute(&admin_context, &Action::GenerateSecurityReport), Ok(()));
assert_eq!(PolicyEngine::can_execute(&admin_context, &Action::ReadUserData { target_user_id: &other_user_id}), Ok(()));
}
#[test]
fn test_status_denials() {
let user_id = Uuid::new_v4();
let admin_roles = &[Role::Admin];
let suspended_context = PolicyContext { user_id, roles: admin_roles, status: &UserStatus::Suspended, trust_score: 1.0 };
let banned_context = PolicyContext { user_id, roles: admin_roles, status: &UserStatus::Banned, trust_score: 1.0 };
assert_eq!(PolicyEngine::can_execute(&suspended_context, &Action::ReadOwnData), Err(PolicyError::UserSuspended));
assert_eq!(PolicyEngine::can_execute(&banned_context, &Action::ReadOwnData), Err(PolicyError::UserBanned));
}
#[test]
fn test_role_from_str() {
assert_eq!(Role::from_str("user").unwrap(), Role::User);
assert_eq!(Role::from_str("ADMIN").unwrap(), Role::Admin); assert!(Role::from_str("guest").is_err());
}
}