road-runner-common 0.11.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
//! Policy Enforcement Point: the one-liner services call per admin endpoint.
//!
//! ```ignore
//! authz::require(&pdp, &user_ctx, "finance:write")?;
//! ```
//!
//! Fail-closed: if no snapshot is loaded yet, this returns 503 (the admin plane is
//! not ready) rather than allowing. The gateway's existing step-up *challenge* flow
//! (401 + `x-auth-step-up-acr`) is the UX mechanism; a `NeedsStepUp` here is a
//! backstop for requests that reached the backend without the required ACR.

use crate::auth::UserContext;
use crate::error::AppError;

use super::pdp::{Decision, Pdp};

/// Enforce that `ctx` may perform `permission`. Maps the PDP decision to an
/// `AppError` (403 for deny/step-up/quorum, 503 when no snapshot is loaded).
pub fn require(pdp: &Pdp, ctx: &UserContext, permission: &str) -> Result<(), AppError> {
    if !pdp.ready() {
        return Err(AppError::ServiceUnavailable {
            message: "authorization service not ready".to_string(),
        });
    }
    match pdp.check(&ctx.subject, ctx.acr.as_deref(), permission) {
        Decision::Allow => Ok(()),
        Decision::Deny { .. } => Err(AppError::Forbidden {
            message: "forbidden".to_string(),
        }),
        Decision::NeedsStepUp { min_acr } => Err(AppError::Forbidden {
            message: format!("step_up_required:{min_acr}"),
        }),
        Decision::NeedsQuorum => Err(AppError::Forbidden {
            message: "quorum_required".to_string(),
        }),
    }
}

/// Like [`require`] but returns the raw [`Decision`] alongside the result, so a
/// caller can emit a decision-audit record for both allow and deny outcomes.
pub fn evaluate(pdp: &Pdp, ctx: &UserContext, permission: &str) -> (Decision, Result<(), AppError>) {
    if !pdp.ready() {
        return (
            Decision::Deny { reason: super::pdp::DenyReason::NoBinding },
            Err(AppError::ServiceUnavailable {
                message: "authorization service not ready".to_string(),
            }),
        );
    }
    let decision = pdp.check(&ctx.subject, ctx.acr.as_deref(), permission);
    let result = match &decision {
        Decision::Allow => Ok(()),
        Decision::Deny { .. } => Err(AppError::Forbidden { message: "forbidden".to_string() }),
        Decision::NeedsStepUp { min_acr } => {
            Err(AppError::Forbidden { message: format!("step_up_required:{min_acr}") })
        }
        Decision::NeedsQuorum => Err(AppError::Forbidden { message: "quorum_required".to_string() }),
    };
    (decision, result)
}