#![allow(clippy::unwrap_used, clippy::panic)]
use chrono::Utc;
use serde_json::json;
use super::{Authorizer, AuthzDecision, AuthzRequest, OperationKind, enforce_authz};
use crate::{
error::{FraiseQLError, Result},
security::SecurityContext,
types::UserId,
};
struct AllowAll;
impl Authorizer for AllowAll {
fn authorize(&self, _req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
Ok(AuthzDecision::Allow)
}
}
struct DenyAll;
impl Authorizer for DenyAll {
fn authorize(&self, _req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
Ok(AuthzDecision::Deny {
reason: "denied".to_string(),
})
}
}
struct RaisingAuthorizer;
impl Authorizer for RaisingAuthorizer {
fn authorize(&self, _req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
Err(FraiseQLError::Validation {
message: "policy backend unreachable".to_string(),
path: None,
})
}
}
struct DenySecret;
impl Authorizer for DenySecret {
fn authorize(&self, req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
if req.name == "secret" {
Ok(AuthzDecision::Deny {
reason: "no access to secret".to_string(),
})
} else {
Ok(AuthzDecision::Allow)
}
}
}
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,
}
}
#[test]
fn allow_all_allows() {
let req = AuthzRequest {
principal: None,
operation: OperationKind::Query,
name: "users",
input: None,
};
assert!(matches!(AllowAll.authorize(&req).unwrap(), AuthzDecision::Allow));
}
#[test]
fn deny_all_denies_with_reason() {
let req = AuthzRequest {
principal: None,
operation: OperationKind::Mutation,
name: "createUser",
input: None,
};
match DenyAll.authorize(&req).unwrap() {
AuthzDecision::Deny { reason } => assert_eq!(reason, "denied"),
AuthzDecision::Allow => panic!("expected deny"),
}
}
#[test]
fn operation_kind_labels() {
assert_eq!(OperationKind::Query.as_str(), "query");
assert_eq!(OperationKind::Mutation.as_str(), "mutation");
assert_eq!(OperationKind::Subscription.as_str(), "subscription");
}
#[test]
fn enforce_allow_is_ok() {
let ops = [(OperationKind::Query, "users".to_string())];
assert!(enforce_authz(&AllowAll, None, &ops, None).is_ok());
}
#[test]
fn enforce_deny_is_authorization_403() {
let principal = ctx("u1");
let ops = [(OperationKind::Mutation, "createUser".to_string())];
let err = enforce_authz(&DenyAll, Some(&principal), &ops, None).unwrap_err();
match err {
FraiseQLError::Authorization {
message,
action,
resource,
} => {
assert!(message.contains("denied"), "reason folded into message: {message}");
assert_eq!(action.as_deref(), Some("mutation"));
assert_eq!(resource.as_deref(), Some("createUser"));
},
other => panic!("expected Authorization, got {other:?}"),
}
}
#[test]
fn enforce_raising_fails_closed_to_403() {
let ops = [(OperationKind::Query, "users".to_string())];
let err = enforce_authz(&RaisingAuthorizer, None, &ops, None).unwrap_err();
assert!(
matches!(err, FraiseQLError::Authorization { .. }),
"raising authorizer must fail closed to Authorization/403, got {err:?}"
);
if let FraiseQLError::Authorization { message, .. } = err {
assert!(
!message.contains("backend unreachable"),
"policy error must not leak: {message}"
);
}
}
#[test]
fn enforce_multi_root_denies_on_any() {
let ops = [
(OperationKind::Query, "public".to_string()),
(OperationKind::Query, "secret".to_string()),
];
let err = enforce_authz(&DenySecret, None, &ops, None).unwrap_err();
match err {
FraiseQLError::Authorization { resource, .. } => {
assert_eq!(resource.as_deref(), Some("secret"), "denied root is the secret one");
},
other => panic!("expected Authorization, got {other:?}"),
}
}
#[test]
fn enforce_passes_input_and_principal() {
struct NeedsInput;
impl Authorizer for NeedsInput {
fn authorize(&self, req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
let ok = req.principal.is_some()
&& req.input.and_then(|v| v.get("ok")).and_then(serde_json::Value::as_bool)
== Some(true);
if ok {
Ok(AuthzDecision::Allow)
} else {
Ok(AuthzDecision::Deny {
reason: "missing input".to_string(),
})
}
}
}
let principal = ctx("u1");
let ops = [(OperationKind::Query, "users".to_string())];
let input = json!({ "ok": true });
assert!(enforce_authz(&NeedsInput, Some(&principal), &ops, Some(&input)).is_ok());
assert!(enforce_authz(&NeedsInput, None, &ops, Some(&input)).is_err());
}