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());
}
}