Use Guard Decorator Guide
Armature provides the #[use_guard] and #[guard] decorators for protecting routes with authorization checks. Guards determine whether a request should be allowed to proceed before the handler executes.
Table of Contents
- Overview
- Features
- Guards vs Middleware
- Basic Usage
- Multiple Guards
- Guard with Configuration
- Controller-Level Guards
- Built-in Guards
- Custom Guards
- Best Practices
- API Reference
- Summary
Overview
Guards are authorization checks that run before a route handler. They answer the question: "Should this request be allowed to proceed?"
- Return
Ok(true)โ Request proceeds to handler - Return
Ok(false)โ Request is denied (403 Forbidden) - Return
Err(e)โ Request fails with the error
Features
- โ Route-level guard protection
- โ Controller-level guard inheritance
- โ Multiple guard chaining (all must pass)
- โ Type-safe guard configuration
- โ Access to request context (headers, params)
- โ Works with all HTTP method decorators
Guards vs Middleware
| Aspect | Guards | Middleware |
|---|---|---|
| Purpose | Authorization (allow/deny) | Request/Response processing |
| Return | Result<bool, Error> |
Result<HttpResponse, Error> |
| Response modification | No | Yes |
| Short-circuit on deny | Yes | Optional |
| Use case | Auth, permissions, rate limiting | Logging, CORS, compression |
Use guards when you need to allow or deny access. Use middleware when you need to process requests/responses.
Basic Usage
Simple Type-Based Guard
When your guard implements Default:
use armature_framework::{get, use_guard};
use armature_core::{HttpRequest, HttpResponse, Error, guard::AuthenticationGuard};
#[use_guard(AuthenticationGuard)]
#[get("/protected")]
async fn protected_endpoint(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok().with_json(&serde_json::json!({
"message": "You have access!"
}))?)
}
Guard Order
Guards are checked in order. If any guard fails, subsequent guards are not checked:
#[use_guard(AuthenticationGuard, AdminGuard)]
#[get("/admin")]
async fn admin_endpoint(req: HttpRequest) -> Result<HttpResponse, Error> {
// Only reaches here if BOTH guards pass
Ok(HttpResponse::ok())
}
Multiple Guards
Chain multiple guards - all must pass:
use armature_framework::{get, use_guard};
use armature_core::{
HttpRequest, HttpResponse, Error,
guard::{AuthenticationGuard, RolesGuard}
};
#[derive(Default)]
struct PremiumGuard;
#[async_trait::async_trait]
impl armature_core::guard::Guard for PremiumGuard {
async fn can_activate(
&self,
context: &armature_core::guard::GuardContext
) -> Result<bool, Error> {
// Check if user has premium subscription
Ok(context.get_header("x-premium-user").is_some())
}
}
#[use_guard(AuthenticationGuard, PremiumGuard)]
#[get("/premium-content")]
async fn premium_content(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
Guard with Configuration
Use #[guard(...)] for guards that need configuration:
use armature_framework::{get, guard};
use armature_core::{HttpRequest, HttpResponse, Error, guard::ApiKeyGuard};
#[guard(ApiKeyGuard::new(vec!["secret-key-1".into(), "secret-key-2".into()]))]
#[get("/api/data")]
async fn api_data(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
Combining Type and Instance Guards
use armature_framework::{get, guard};
use armature_core::{
HttpRequest, HttpResponse, Error,
guard::{AuthenticationGuard, RolesGuard}
};
#[guard(
AuthenticationGuard,
RolesGuard::new(vec!["admin".into(), "moderator".into()])
)]
#[get("/moderation")]
async fn moderation_panel(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
Controller-Level Guards
Apply guards to all routes in a controller:
use armature_framework::{controller, get, post, guard};
use armature_core::{HttpRequest, HttpResponse, Error, guard::AuthenticationGuard};
#[guard(AuthenticationGuard)]
#[controller("/api/users")]
struct UserController;
impl UserController {
// All routes require authentication
#[get("")]
async fn list_users(&self, req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
#[get("/:id")]
async fn get_user(&self, req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
#[post("")]
async fn create_user(&self, req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::created())
}
}
Combining Controller and Route Guards
Route guards add to controller guards:
use armature_framework::{controller, get, guard, use_guard};
use armature_core::{
HttpRequest, HttpResponse, Error,
guard::AuthenticationGuard
};
#[derive(Default)]
struct AdminGuard;
#[async_trait::async_trait]
impl armature_core::guard::Guard for AdminGuard {
async fn can_activate(
&self,
context: &armature_core::guard::GuardContext
) -> Result<bool, Error> {
// Check for admin role
Ok(context.get_header("x-user-role")
.map(|r| r == "admin")
.unwrap_or(false))
}
}
#[guard(AuthenticationGuard)] // All routes require auth
#[controller("/api")]
struct ApiController;
impl ApiController {
#[get("/public")]
async fn public_data(&self, req: HttpRequest) -> Result<HttpResponse, Error> {
// Only needs AuthenticationGuard
Ok(HttpResponse::ok())
}
#[use_guard(AdminGuard)] // Additional guard
#[get("/admin")]
async fn admin_data(&self, req: HttpRequest) -> Result<HttpResponse, Error> {
// Needs AuthenticationGuard + AdminGuard
Ok(HttpResponse::ok())
}
}
Built-in Guards
Armature provides several built-in guards:
| Guard | Purpose | Default |
|---|---|---|
AuthenticationGuard |
Check for Bearer token | โ |
RolesGuard |
Check user roles | โ (needs roles) |
ApiKeyGuard |
Validate API keys | โ (needs keys) |
CustomGuard<F> |
Custom predicate | โ (needs fn) |
AuthenticationGuard
Checks for Authorization: Bearer <token> header:
#[use_guard(AuthenticationGuard)]
#[get("/me")]
async fn get_current_user(req: HttpRequest) -> Result<HttpResponse, Error> {
let token = req.headers.get("authorization")
.and_then(|h| h.strip_prefix("Bearer "))
.unwrap(); // Safe after guard passes
Ok(HttpResponse::ok())
}
RolesGuard
Checks for specific roles (requires role extraction logic):
#[guard(RolesGuard::new(vec!["admin".into()]))]
#[get("/admin/settings")]
async fn admin_settings(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
ApiKeyGuard
Validates X-API-Key header:
#[guard(ApiKeyGuard::new(vec![
"key-production".into(),
"key-staging".into()
]))]
#[get("/api/v1/data")]
async fn api_data(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
Custom Guards
Create custom guards by implementing the Guard trait:
use armature_core::{Error, guard::{Guard, GuardContext}};
use async_trait::async_trait;
/// Rate limiting guard
pub struct RateLimitGuard {
max_requests: u32,
}
impl RateLimitGuard {
pub fn new(max_requests: u32) -> Self {
Self { max_requests }
}
}
#[async_trait]
impl Guard for RateLimitGuard {
async fn can_activate(&self, context: &GuardContext) -> Result<bool, Error> {
// Get client identifier (IP, API key, etc.)
let client_id = context.get_header("x-client-id")
.cloned()
.unwrap_or_else(|| "anonymous".into());
// Check rate limit (implement your logic)
let request_count = get_request_count(&client_id).await;
if request_count >= self.max_requests {
Err(Error::TooManyRequests(format!(
"Rate limit exceeded: {} requests",
self.max_requests
)))
} else {
increment_request_count(&client_id).await;
Ok(true)
}
}
}
Guard with Request Data Access
Guards have full access to the request context:
pub struct OwnershipGuard;
impl Default for OwnershipGuard {
fn default() -> Self {
Self
}
}
#[async_trait]
impl Guard for OwnershipGuard {
async fn can_activate(&self, context: &GuardContext) -> Result<bool, Error> {
// Access path parameters
let resource_id = context.get_param("id")
.ok_or_else(|| Error::BadRequest("Missing resource ID".into()))?;
// Access headers
let user_id = context.get_header("x-user-id")
.ok_or_else(|| Error::Unauthorized("Missing user ID".into()))?;
// Check ownership
let resource = fetch_resource(resource_id).await?;
if resource.owner_id == *user_id {
Ok(true)
} else {
Err(Error::Forbidden("You don't own this resource".into()))
}
}
}
IP Whitelist Guard
pub struct IpWhitelistGuard {
allowed_ips: Vec<String>,
}
impl IpWhitelistGuard {
pub fn new(ips: Vec<String>) -> Self {
Self { allowed_ips: ips }
}
}
#[async_trait]
impl Guard for IpWhitelistGuard {
async fn can_activate(&self, context: &GuardContext) -> Result<bool, Error> {
let client_ip = context.get_header("x-forwarded-for")
.or_else(|| context.get_header("x-real-ip"))
.ok_or_else(|| Error::BadRequest("Cannot determine client IP".into()))?;
if self.allowed_ips.contains(client_ip) {
Ok(true)
} else {
Err(Error::Forbidden(format!(
"IP {} not in whitelist",
client_ip
)))
}
}
}
Time-Based Guard
use chrono::{Local, Timelike};
pub struct BusinessHoursGuard;
impl Default for BusinessHoursGuard {
fn default() -> Self {
Self
}
}
#[async_trait]
impl Guard for BusinessHoursGuard {
async fn can_activate(&self, _context: &GuardContext) -> Result<bool, Error> {
let now = Local::now();
let hour = now.hour();
// Allow access only during business hours (9 AM - 5 PM)
if hour >= 9 && hour < 17 {
Ok(true)
} else {
Err(Error::Forbidden(
"This endpoint is only available during business hours".into()
))
}
}
}
Best Practices
1. Keep Guards Focused
Each guard should check one thing:
// โ
Good: Single responsibility
pub struct AuthenticationGuard; // Only checks auth
pub struct AdminRoleGuard; // Only checks admin role
pub struct ResourceOwnerGuard; // Only checks ownership
// โ Bad: Multiple responsibilities
pub struct EverythingGuard; // Checks auth + role + ownership + ...
2. Use Descriptive Errors
// โ
Good: Helpful error messages
Err(Error::Forbidden("Admin role required to access this resource".into()))
Err(Error::Unauthorized("API key expired".into()))
// โ Bad: Generic errors
Err(Error::Forbidden("Access denied".into()))
3. Order Guards Logically
// โ
Good: Check auth before permissions
#[use_guard(AuthenticationGuard, AdminGuard)]
// โ Bad: Checking permissions before auth
#[use_guard(AdminGuard, AuthenticationGuard)]
4. Implement Default for Simple Guards
// โ
Good: Allows #[use_guard(MyGuard)]
impl Default for MyGuard {
fn default() -> Self {
Self
}
}
// Then use as:
#[use_guard(MyGuard)]
5. Use Configuration for Complex Guards
// โ
Good: Use #[guard(...)] for configured guards
#[guard(RolesGuard::new(vec!["admin".into()]))]
// Instead of:
// #[use_guard(RolesGuard)] // Won't work without Default
Common Pitfalls
โ Guard Without Default
// This will fail if RolesGuard doesn't implement Default
#[use_guard(RolesGuard)] // Error!
// Use #[guard(...)] instead for configured guards
#[guard(RolesGuard::new(vec!["admin".into()]))] // Works!
โ Returning Ok(false) Without Error
// Bad: Returns generic 403
async fn can_activate(&self, _ctx: &GuardContext) -> Result<bool, Error> {
Ok(false) // User gets "Access denied by guard"
}
// Good: Return specific error
async fn can_activate(&self, _ctx: &GuardContext) -> Result<bool, Error> {
Err(Error::Forbidden("Premium subscription required".into()))
}
โ Heavy Operations in Guards
// Bad: Database query on every request
async fn can_activate(&self, ctx: &GuardContext) -> Result<bool, Error> {
let user = db.fetch_user_with_all_relations().await?; // Expensive!
Ok(user.has_permission("admin"))
}
// Good: Use cached data or lightweight checks
async fn can_activate(&self, ctx: &GuardContext) -> Result<bool, Error> {
// Check JWT claims instead of database
let role = ctx.get_header("x-user-role");
Ok(role == Some(&"admin".into()))
}
API Reference
Decorators
| Decorator | Target | Description |
|---|---|---|
#[use_guard(Type, ...)] |
Function | Apply guards by type (requires Default) |
#[guard(expr, ...)] |
Function/Struct | Apply guard instances with configuration |
GuardContext Methods
| Method | Description |
|---|---|
get_header(name) |
Get request header by name |
get_param(name) |
Get path parameter by name |
request |
Access full HttpRequest |
Built-in Guard Types
| Type | Constructor | Description |
|---|---|---|
AuthenticationGuard |
(unit struct) | Bearer token check |
RolesGuard |
new(roles: Vec<String>) |
Role-based access |
ApiKeyGuard |
new(keys: Vec<String>) |
API key validation |
CustomGuard<F> |
new(predicate) |
Custom predicate |
Summary
Key Points:
#[use_guard(Type)]for guards implementingDefault#[guard(expr)]for guards with configuration- Guards return
Result<bool, Error>:Ok(true)โ AllowOk(false)โ Deny (generic 403)Err(e)โ Deny with specific error
- All guards must pass (AND logic)
- Guards run in order; first failure stops execution
- Use guards for authorization, middleware for processing
Quick Reference:
// Simple guard (Default required)
#[use_guard(AuthenticationGuard)]
#[get("/protected")]
async fn protected(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
// Configured guard
#[guard(ApiKeyGuard::new(vec!["key1".into()]))]
#[get("/api")]
async fn api(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
// Multiple guards
#[use_guard(AuthenticationGuard, AdminGuard)]
#[get("/admin")]
async fn admin(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
// Controller-level guard
#[guard(AuthenticationGuard)]
#[controller("/api")]
struct ApiController;