feat: add permission checker module
Checks: - User is logged in - Role is allowed for action - Verification status - Account status Includes unit tests.
This commit is contained in:
parent
eaec788b2f
commit
89e4809f0b
2 changed files with 182 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ mod error;
|
||||||
mod forms;
|
mod forms;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
|
mod permissions;
|
||||||
mod providers;
|
mod providers;
|
||||||
mod retrieval;
|
mod retrieval;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
|
||||||
181
src/permissions/mod.rs
Normal file
181
src/permissions/mod.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::actions::ActionDefinition;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PermissionContext {
|
||||||
|
pub user_id: Option<String>,
|
||||||
|
pub role: Option<String>,
|
||||||
|
pub is_verified: bool,
|
||||||
|
pub account_status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PermissionResult {
|
||||||
|
pub allowed: bool,
|
||||||
|
pub reason_code: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PermissionChecker;
|
||||||
|
|
||||||
|
impl PermissionChecker {
|
||||||
|
pub fn check(context: &PermissionContext, action: &ActionDefinition) -> PermissionResult {
|
||||||
|
if !context.user_id.is_some() && action.requires_login {
|
||||||
|
return PermissionResult {
|
||||||
|
allowed: false,
|
||||||
|
reason_code: "NOT_LOGGED_IN".to_string(),
|
||||||
|
message: "You must be logged in to perform this action.".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(role) = &context.role {
|
||||||
|
if !action.allowed_roles.contains(role) {
|
||||||
|
return PermissionResult {
|
||||||
|
allowed: false,
|
||||||
|
reason_code: "ROLE_NOT_ALLOWED".to_string(),
|
||||||
|
message: format!(
|
||||||
|
"This action is not available for your role ({}).",
|
||||||
|
role
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if action.requires_login {
|
||||||
|
return PermissionResult {
|
||||||
|
allowed: false,
|
||||||
|
reason_code: "NO_ROLE_SELECTED".to_string(),
|
||||||
|
message: "Please select a role before performing this action.".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if action.requires_verification && !context.is_verified {
|
||||||
|
return PermissionResult {
|
||||||
|
allowed: false,
|
||||||
|
reason_code: "VERIFICATION_REQUIRED".to_string(),
|
||||||
|
message: "You need to complete verification before using this feature.".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.account_status != "ACTIVE" {
|
||||||
|
return PermissionResult {
|
||||||
|
allowed: false,
|
||||||
|
reason_code: "ACCOUNT_NOT_ACTIVE".to_string(),
|
||||||
|
message: format!(
|
||||||
|
"Your account is {}. Please contact support.",
|
||||||
|
context.account_status.to_lowercase()
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
PermissionResult {
|
||||||
|
allowed: true,
|
||||||
|
reason_code: "ALLOWED".to_string(),
|
||||||
|
message: "Permission granted.".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_missing_fields(
|
||||||
|
provided_fields: &[String],
|
||||||
|
required_fields: &[String],
|
||||||
|
) -> Vec<String> {
|
||||||
|
required_fields
|
||||||
|
.iter()
|
||||||
|
.filter(|f| !provided_fields.contains(f))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_fields(
|
||||||
|
fields: &serde_json::Value,
|
||||||
|
required_fields: &[String],
|
||||||
|
optional_fields: &[String],
|
||||||
|
) -> (bool, Vec<String>) {
|
||||||
|
let mut missing = Vec::new();
|
||||||
|
let obj = fields.as_object().cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
for field in required_fields {
|
||||||
|
if !obj.contains_key(field) || obj.get(field).map(|v| v.is_null()).unwrap_or(false) {
|
||||||
|
missing.push(field.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(missing.is_empty(), missing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn get_test_action() -> ActionDefinition {
|
||||||
|
ActionDefinition {
|
||||||
|
action_code: "test_action".to_string(),
|
||||||
|
intent: "test_action".to_string(),
|
||||||
|
allowed_roles: vec!["COMPANY".to_string(), "JOB_SEEKER".to_string()],
|
||||||
|
requires_login: true,
|
||||||
|
requires_verification: true,
|
||||||
|
requires_confirmation: true,
|
||||||
|
required_fields: vec!["field1".to_string(), "field2".to_string()],
|
||||||
|
optional_fields: vec!["opt1".to_string()],
|
||||||
|
feature_code: "test".to_string(),
|
||||||
|
ai_action_cost: 1,
|
||||||
|
uses_llm: true,
|
||||||
|
backend_handler: "test.handler".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allows_verified_company_user() {
|
||||||
|
let context = PermissionContext {
|
||||||
|
user_id: Some("user123".to_string()),
|
||||||
|
role: Some("COMPANY".to_string()),
|
||||||
|
is_verified: true,
|
||||||
|
account_status: "ACTIVE".to_string(),
|
||||||
|
};
|
||||||
|
let action = get_test_action();
|
||||||
|
let result = PermissionChecker::check(&context, &action);
|
||||||
|
assert!(result.allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_blocks_unverified_user() {
|
||||||
|
let context = PermissionContext {
|
||||||
|
user_id: Some("user123".to_string()),
|
||||||
|
role: Some("COMPANY".to_string()),
|
||||||
|
is_verified: false,
|
||||||
|
account_status: "ACTIVE".to_string(),
|
||||||
|
};
|
||||||
|
let action = get_test_action();
|
||||||
|
let result = PermissionChecker::check(&context, &action);
|
||||||
|
assert!(!result.allowed);
|
||||||
|
assert_eq!(result.reason_code, "VERIFICATION_REQUIRED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_blocks_wrong_role() {
|
||||||
|
let context = PermissionContext {
|
||||||
|
user_id: Some("user123".to_string()),
|
||||||
|
role: Some("ADMIN".to_string()),
|
||||||
|
is_verified: true,
|
||||||
|
account_status: "ACTIVE".to_string(),
|
||||||
|
};
|
||||||
|
let action = get_test_action();
|
||||||
|
let result = PermissionChecker::check(&context, &action);
|
||||||
|
assert!(!result.allowed);
|
||||||
|
assert_eq!(result.reason_code, "ROLE_NOT_ALLOWED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_login() {
|
||||||
|
let context = PermissionContext {
|
||||||
|
user_id: None,
|
||||||
|
role: None,
|
||||||
|
is_verified: false,
|
||||||
|
account_status: "ACTIVE".to_string(),
|
||||||
|
};
|
||||||
|
let action = get_test_action();
|
||||||
|
let result = PermissionChecker::check(&context, &action);
|
||||||
|
assert!(!result.allowed);
|
||||||
|
assert_eq!(result.reason_code, "NOT_LOGGED_IN");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue