#![allow(clippy::unwrap_used, clippy::panic)]
use chrono::Utc;
use serde_json::json;
use super::{FieldAuthorizer, FieldAuthzDecision, FieldAuthzRequest};
use crate::{
error::{FraiseQLError, Result},
schema::FieldDenyPolicy,
security::SecurityContext,
types::UserId,
};
struct AllowAll;
impl FieldAuthorizer for AllowAll {
fn authorize_field(&self, _req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
Ok(FieldAuthzDecision::Allow)
}
}
struct DenyAll;
impl FieldAuthorizer for DenyAll {
fn authorize_field(&self, _req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
Ok(FieldAuthzDecision::Deny {
code: "denied".to_string(),
on_deny: FieldDenyPolicy::Reject,
})
}
}
struct RaisingFieldAuthorizer;
impl FieldAuthorizer for RaisingFieldAuthorizer {
fn authorize_field(&self, _req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
Err(FraiseQLError::Validation {
message: "policy backend unreachable".to_string(),
path: None,
})
}
}
struct OwnerOnly;
impl FieldAuthorizer for OwnerOnly {
fn authorize_field(&self, req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
let owner = req.parent.and_then(|p| p.get("owner_id")).and_then(|v| v.as_str());
if owner == Some(req.principal.user_id.as_str()) {
Ok(FieldAuthzDecision::Allow)
} else {
Ok(FieldAuthzDecision::Deny {
code: "not_owner".to_string(),
on_deny: FieldDenyPolicy::Mask,
})
}
}
}
fn ctx(user_id: &str) -> SecurityContext {
SecurityContext {
user_id: UserId::new(user_id),
roles: vec![],
tenant_id: None,
scopes: vec![],
attributes: std::collections::HashMap::new(),
request_id: "req-test".to_string(),
ip_address: None,
authenticated_at: Utc::now(),
expires_at: Utc::now(),
issuer: None,
audience: None,
email: None,
display_name: None,
}
}
fn request<'a>(
principal: &'a SecurityContext,
parent: &'a serde_json::Value,
) -> FieldAuthzRequest<'a> {
FieldAuthzRequest {
principal,
type_name: "User",
field_name: "email",
parent: Some(parent),
arguments: None,
}
}
#[test]
fn allow_all_allows() {
let principal = ctx("u1");
let parent = json!({ "owner_id": "u2" });
let decision = AllowAll.authorize_field(&request(&principal, &parent)).unwrap();
assert!(matches!(decision, FieldAuthzDecision::Allow));
}
#[test]
fn deny_all_rejects() {
let principal = ctx("u1");
let parent = json!({});
let decision = DenyAll.authorize_field(&request(&principal, &parent)).unwrap();
match decision {
FieldAuthzDecision::Deny { code, on_deny } => {
assert_eq!(code, "denied");
assert_eq!(on_deny, FieldDenyPolicy::Reject);
},
FieldAuthzDecision::Allow => panic!("expected deny"),
}
}
#[test]
fn raising_authorizer_returns_err() {
let principal = ctx("u1");
let parent = json!({});
let result = RaisingFieldAuthorizer.authorize_field(&request(&principal, &parent));
assert!(result.is_err(), "raising authorizer must return Err for fail-closed handling");
}
#[test]
fn owner_only_allows_owner() {
let principal = ctx("u1");
let parent = json!({ "owner_id": "u1" });
let decision = OwnerOnly.authorize_field(&request(&principal, &parent)).unwrap();
assert!(matches!(decision, FieldAuthzDecision::Allow));
}
#[test]
fn owner_only_masks_non_owner() {
let principal = ctx("u1");
let parent = json!({ "owner_id": "u2" });
let decision = OwnerOnly.authorize_field(&request(&principal, &parent)).unwrap();
match decision {
FieldAuthzDecision::Deny { code, on_deny } => {
assert_eq!(code, "not_owner");
assert_eq!(on_deny, FieldDenyPolicy::Mask);
},
FieldAuthzDecision::Allow => panic!("expected deny for non-owner"),
}
}