Axum integration
Add strongly typed authorization to an Axum service — the check itself is a single line in your handler.
Praetor ships an optional, tiny Axum integration behind the axum feature. It does one thing: hand your handler the Ability your middleware built, so you can call authorize and let the compiler check your rules. Praetor still never validates tokens — that stays in your application, exactly where it belongs.
This guide assumes you have read the main guide and know how to model actions, subjects, and claims. Here we focus purely on wiring an ability through an Axum request.
Enable the feature
The integration is off by default, so the core library stays dependency-free. Turn it on with the axum feature:
cargo add praetor --features axum
Or in your Cargo.toml directly:
[dependencies]
praetor = { version = "0.1", features = ["axum"] }
praetor::axum; the rest of the library is unchanged.How it fits together
The division of labor is the whole idea. Your application does the authentication; Praetor does the authorization; the feature is just the thin seam between them.
Your middleware
Validate the token, decode your Claims, call build_ability, and insert the ability into the request as an Arc.
Praetor's extractor
Praetor<T> pulls that shared ability back out in your handler so you can authorize against the resource you loaded.
Concretely, a request flows through four steps — the first three are yours, the fourth is one line:
- Middleware validates the request and builds an ability from the claims.
- Middleware inserts the ability into the request extensions.
- The handler extracts it with
Praetor<AppAbility>. - The handler authorizes:
ability.authorize(action, &resource)?.
1 · Define your rules
This is where Praetor earns its keep. Your entire authorization model is one function that turns claims into an ability — ordinary Rust, checked by the compiler, with no policy files or DSL. Anyone may read messages; only the author may update their own:
use praetor::{Ability, AbilitySubject};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Action { Read, Update }
#[derive(Clone, Copy, PartialEq, Eq)]
enum Subject { Message }
#[derive(Clone)]
struct Claims { user_id: u64 }
#[derive(Clone)]
struct Message { id: u64, author_id: u64, body: String }
impl AbilitySubject<Subject> for Message {
fn subject_type(&self) -> Subject { Subject::Message }
}
// A type alias keeps handler signatures short.
type AppAbility = Ability<Action, Subject, Claims>;
// Your whole authorization model: claims in, ability out.
fn build_ability(claims: Claims) -> AppAbility {
Ability::builder(claims)
.can(Action::Read, Subject::Message)
.can_if(Action::Update, Subject::Message, |claims, m: &Message| {
claims.user_id == m.author_id
})
.build()
}
|claims, m: &Message| claims.user_id == m.author_id is normal, type-checked Rust — no query language, no reflection. Grow it however your model needs.2 · Insert the ability
Your middleware validates the request the way it always has, then builds the ability and stores it in the request extensions as an Arc. Praetor never inspects the token itself:
use std::sync::Arc;
use axum::{extract::Request, middleware::Next, response::Response};
async fn auth(mut request: Request, next: Next) -> Response {
// Pretend we validated a JWT and decoded these claims.
let claims = Claims { user_id: 1 };
request.extensions_mut().insert(Arc::new(build_ability(claims)));
next.run(request).await
}
Arc? An ability holds your rule closures, so it is shared rather than cloned. Wrapping it in Arc lets every handler extract it cheaply — the extractor only clones the pointer.3 · Extract & authorize
Add the Praetor<AppAbility> extractor to your handler. It hands you the shared ability, which derefs to the underlying Ability — so the authorization check is a single ? line after you load the resource:
use axum::extract::{Path, State};
use praetor::axum::Praetor;
async fn update_message(
Praetor(ability): Praetor<AppAbility>,
Path(id): Path<u64>,
State(state): State<AppState>,
) -> Result<String, ApiError> {
let message = state.messages.get(&id).cloned().ok_or(ApiError::NotFound)?;
ability.authorize(Action::Update, &message)?; // <- the whole check
Ok(format!("updated message {}", message.id))
}
Map Praetor's Forbidden onto your own error type so ? turns a denied request into an HTTP 403:
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
enum ApiError { NotFound, Forbidden }
impl From<praetor::Forbidden> for ApiError {
fn from(_: praetor::Forbidden) -> Self { ApiError::Forbidden }
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "not found").into_response(),
ApiError::Forbidden => (StatusCode::FORBIDDEN, "forbidden").into_response(),
}
}
}
Praetor(ability) gives you an Arc<AppAbility>. Because it derefs to the ability, you call ability.authorize(..), ability.can(..), and friends directly — no unwrapping.4 · Wire up the router
Attach the middleware as a layer so every routed request gets an ability before it reaches a handler:
use axum::{Router, middleware, routing::get};
let app = Router::new()
.route("/messages/{id}", get(update_message))
.layer(middleware::from_fn(auth))
.with_state(state);
That is the entire integration: define rules once, insert the ability in middleware, and authorize in one line per handler.
Missing abilities
If a handler using Praetor<T> runs without the middleware ever inserting an ability, extraction fails with MissingAbility, which converts into a 500 Internal Server Error. That is deliberate: a missing ability means the layer was not wired up, which is a server misconfiguration rather than a client problem.
401 Unauthorized. Praetor stays out of that policy decision.Run the example
The repository ships a complete, runnable version of everything above in examples/axum.rs. It builds the router, drives two requests in-process, and prints the results — 200 for the owner and 403 for someone else:
cargo run --example axum --features axum
update own message: 200 OK updated message 1: mine
update others message: 403 Forbidden forbidden
What's next
You now have authorization wired through an Axum request end to end. From here:
- Revisit the main guide for the full rule model — conditional denies, precedence, and heterogeneous resources.
- Browse the
praetor::axumAPI on docs.rs. - Read the source and open issues on GitHub.