use axum::{
extract::{Path, Query, State},
http::HeaderMap,
Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::callback::AuthCallback;
use crate::errors::{AppError, ERR_ADMIN_REQUIRED};
use crate::models::MessageResponse;
use crate::repositories::pagination::{cap_limit, cap_offset};
use crate::repositories::{AbacPolicy, OrgRole, PolicyConditions, PolicyEffect};
use crate::services::EmailService;
use crate::utils::authenticate;
use crate::AppState;
#[derive(Debug, Deserialize)]
pub struct CreatePolicyRequest {
pub name: String,
#[serde(default)]
pub description: Option<String>,
pub permission: String,
pub conditions: PolicyConditions,
pub effect: PolicyEffect,
#[serde(default)]
pub priority: i32,
}
#[derive(Debug, Deserialize)]
pub struct UpdatePolicyRequest {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub permission: Option<String>,
#[serde(default)]
pub conditions: Option<PolicyConditions>,
#[serde(default)]
pub effect: Option<PolicyEffect>,
#[serde(default)]
pub priority: Option<i32>,
#[serde(default)]
pub enabled: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct PolicyResponse {
pub id: Uuid,
pub org_id: Uuid,
pub name: String,
pub description: Option<String>,
pub permission: String,
pub conditions: PolicyConditions,
pub effect: PolicyEffect,
pub priority: i32,
pub enabled: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<AbacPolicy> for PolicyResponse {
fn from(policy: AbacPolicy) -> Self {
Self {
id: policy.id,
org_id: policy.org_id,
name: policy.name,
description: policy.description,
permission: policy.permission,
conditions: policy.conditions,
effect: policy.effect,
priority: policy.priority,
enabled: policy.enabled,
created_at: policy.created_at.to_rfc3339(),
updated_at: policy.updated_at.to_rfc3339(),
}
}
}
#[derive(Debug, Serialize)]
pub struct ListPoliciesResponse {
pub policies: Vec<PolicyResponse>,
pub total: usize,
}
#[derive(Debug, Deserialize)]
pub struct ListPoliciesQueryParams {
#[serde(default = "default_limit")]
pub limit: u32,
#[serde(default)]
pub offset: u32,
}
fn default_limit() -> u32 {
50
}
async fn verify_org_admin<C: AuthCallback, E: EmailService>(
state: &Arc<AppState<C, E>>,
headers: &HeaderMap,
org_id: Uuid,
) -> Result<Uuid, AppError> {
let auth = authenticate(state, headers).await?;
if let Some(membership) = state
.membership_repo
.find_by_user_and_org(auth.user_id, org_id)
.await?
{
if membership.role.has_at_least(OrgRole::Admin) {
return Ok(auth.user_id);
}
}
if let Some(user) = state.user_repo.find_by_id(auth.user_id).await? {
if user.is_system_admin {
return Ok(auth.user_id);
}
}
Err(AppError::Forbidden(ERR_ADMIN_REQUIRED.into()))
}
pub async fn list_policies<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path(org_id): Path<Uuid>,
Query(params): Query<ListPoliciesQueryParams>,
) -> Result<Json<ListPoliciesResponse>, AppError> {
verify_org_admin(&state, &headers, org_id).await?;
let limit = cap_limit(params.limit);
let offset = cap_offset(params.offset);
let (policies_result, total_result) = tokio::join!(
state.policy_repo.find_by_org_paged(org_id, limit, offset),
state.policy_repo.count_by_org(org_id)
);
let policies = policies_result?;
let total = total_result?;
Ok(Json(ListPoliciesResponse {
policies: policies.into_iter().map(PolicyResponse::from).collect(),
total: total as usize,
}))
}
pub async fn create_policy<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path(org_id): Path<Uuid>,
Json(req): Json<CreatePolicyRequest>,
) -> Result<Json<PolicyResponse>, AppError> {
verify_org_admin(&state, &headers, org_id).await?;
let policy = AbacPolicy::new(org_id, &req.name, &req.permission, req.effect)
.with_conditions(req.conditions)
.with_priority(req.priority);
let mut policy = policy;
policy.description = req.description;
let created = state.policy_repo.create(policy).await?;
Ok(Json(PolicyResponse::from(created)))
}
pub async fn get_policy<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path((org_id, policy_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<PolicyResponse>, AppError> {
verify_org_admin(&state, &headers, org_id).await?;
let policy = state
.policy_repo
.find_by_id(policy_id)
.await?
.ok_or(AppError::NotFound("Policy not found".into()))?;
if policy.org_id != org_id {
return Err(AppError::NotFound("Policy not found".into()));
}
Ok(Json(PolicyResponse::from(policy)))
}
pub async fn update_policy<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path((org_id, policy_id)): Path<(Uuid, Uuid)>,
Json(req): Json<UpdatePolicyRequest>,
) -> Result<Json<PolicyResponse>, AppError> {
verify_org_admin(&state, &headers, org_id).await?;
let mut policy = state
.policy_repo
.find_by_id(policy_id)
.await?
.ok_or(AppError::NotFound("Policy not found".into()))?;
if policy.org_id != org_id {
return Err(AppError::NotFound("Policy not found".into()));
}
if let Some(name) = req.name {
policy.name = name;
}
if let Some(description) = req.description {
policy.description = Some(description);
}
if let Some(permission) = req.permission {
policy.permission = permission;
}
if let Some(conditions) = req.conditions {
policy.conditions = conditions;
}
if let Some(effect) = req.effect {
policy.effect = effect;
}
if let Some(priority) = req.priority {
policy.priority = priority;
}
if let Some(enabled) = req.enabled {
policy.enabled = enabled;
}
let updated = state.policy_repo.update(policy).await?;
Ok(Json(PolicyResponse::from(updated)))
}
pub async fn delete_policy<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path((org_id, policy_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<MessageResponse>, AppError> {
verify_org_admin(&state, &headers, org_id).await?;
let policy = state
.policy_repo
.find_by_id(policy_id)
.await?
.ok_or(AppError::NotFound("Policy not found".into()))?;
if policy.org_id != org_id {
return Err(AppError::NotFound("Policy not found".into()));
}
state.policy_repo.delete(policy_id).await?;
Ok(Json(MessageResponse {
message: "Policy deleted successfully".into(),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_policy_request_deserialize() {
let json = r#"{
"name": "Owner Delete",
"description": "Allow owners to delete their resources",
"permission": "project:delete",
"conditions": {
"resource": {
"owner_id": {"type": "equals", "value": "${subject.user_id}"}
}
},
"effect": "allow",
"priority": 10
}"#;
let req: CreatePolicyRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.name, "Owner Delete");
assert_eq!(req.effect, PolicyEffect::Allow);
assert_eq!(req.priority, 10);
}
#[test]
fn test_update_policy_request_partial() {
let json = r#"{"name": "Updated Name", "enabled": false}"#;
let req: UpdatePolicyRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.name, Some("Updated Name".to_string()));
assert_eq!(req.enabled, Some(false));
assert!(req.permission.is_none());
}
#[test]
fn test_policy_response_serialization() {
let org_id = Uuid::new_v4();
let policy = AbacPolicy::new(org_id, "Test", "project:read", PolicyEffect::Allow);
let response = PolicyResponse::from(policy);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"name\":\"Test\""));
assert!(json.contains("\"permission\":\"project:read\""));
assert!(json.contains("\"effect\":\"allow\""));
}
#[test]
fn test_list_policies_query_defaults() {
let params: ListPoliciesQueryParams = serde_json::from_str("{}").unwrap();
assert_eq!(params.limit, 50);
assert_eq!(params.offset, 0);
}
}