Rust
Fine-grained permission checking middleware for Axum
use wacht::middleware::{ PermissionLayer, MultiplePermissionLayers, RequireAnyPermissionLayer, PermissionScope };
use axum::{Router, routing::get}; use wacht::middleware::{AuthLayer, PermissionLayer}; let app = Router::new() .route("/admin/users", get(list_users)) .layer(PermissionLayer::organization("users:read")) .layer(AuthLayer::new());
// Organization-level permission let org_layer = PermissionLayer::organization("billing:manage"); // Workspace-level permission let workspace_layer = PermissionLayer::workspace("projects:write");
use wacht::middleware::{PermissionLayer, PermissionScope}; // Using convenience methods let layer = PermissionLayer::organization("admin:read"); let layer = PermissionLayer::workspace("content:write"); // Using constructor with explicit scope let layer = PermissionLayer::new( "custom:permission", PermissionScope::Organization );
use wacht::middleware::{MultiplePermissionLayers, PermissionScope}; // Require ALL permissions (AND logic) let admin_layer = MultiplePermissionLayers::all(vec![ ("users:read", PermissionScope::Organization), ("users:write", PermissionScope::Organization), ("audit:read", PermissionScope::Organization), ]); // Require ANY permission (OR logic) let content_layer = MultiplePermissionLayers::any(vec![ ("content:write", PermissionScope::Workspace), ("content:admin", PermissionScope::Workspace), ]);
use wacht::middleware::{RequireAnyPermissionLayer, PermissionScope}; let layer = RequireAnyPermissionLayer::new(vec![ ("reports:view", PermissionScope::Organization), ("reports:admin", PermissionScope::Organization), ("super:admin", PermissionScope::Organization), ]);
X-Auth-Error
WWW-Authenticate: Bearer
use axum::{Router, routing::get, Json, Extension}; use wacht::middleware::*; use serde_json::{json, Value}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { wacht::init_from_env().await?; let app = Router::new() // Super admin routes - require all permissions .nest("/admin", Router::new() .route("/users", get(manage_users)) .route("/billing", get(manage_billing)) .route("/audit", get(view_audit_logs)) .layer(MultiplePermissionLayers::all(vec![ ("admin:users", PermissionScope::Organization), ("admin:billing", PermissionScope::Organization), ("admin:audit", PermissionScope::Organization), ])) ) // Content management - require any permission .nest("/content", Router::new() .route("/posts", get(list_posts)) .route("/media", get(list_media)) .layer(RequireAnyPermissionLayer::new(vec![ ("content:read", PermissionScope::Workspace), ("content:write", PermissionScope::Workspace), ("content:admin", PermissionScope::Workspace), ])) ) // Reports - specific permission .route("/reports/financial", get(financial_reports)) .layer(PermissionLayer::organization("reports:financial")) // Apply auth to all routes .layer(AuthLayer::new()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, app).await?; Ok(()) } async fn manage_users(Extension(auth): Extension<AuthContext>) -> Json<Value> { Json(json!({ "message": "User management", "user": auth.user_id })) } async fn financial_reports(Extension(auth): Extension<AuthContext>) -> Json<Value> { Json(json!({ "report": "Financial data", "organization": auth.organization_id })) }
use axum::{Router, routing::{get, post, delete}}; use wacht::middleware::*; let workspace_routes = Router::new() // Read-only routes .route("/projects", get(list_projects)) .route("/projects/:id", get(get_project)) .layer(PermissionLayer::workspace("projects:read")) // Write routes .route("/projects", post(create_project)) .route("/projects/:id", delete(delete_project)) .layer(PermissionLayer::workspace("projects:write")) // Apply auth .layer(AuthLayer::new());
use axum::{ extract::{Path, Extension}, http::StatusCode, response::IntoResponse, }; async fn update_resource( Path(resource_id): Path<String>, Extension(auth): Extension<AuthContext>, ) -> impl IntoResponse { // Check workspace-specific permission let required_permission = format!("resource:{}:write", resource_id); let has_permission = auth.workspace_permissions .as_ref() .map(|perms| perms.contains(&required_permission)) .unwrap_or(false); if !has_permission { return ( StatusCode::FORBIDDEN, "Missing required permission" ).into_response(); } // Process the update... (StatusCode::OK, "Resource updated").into_response() }
// Correct order let app = Router::new() .route("/admin", get(admin_handler)) .layer(PermissionLayer::organization("admin:access")) // Second .layer(AuthLayer::new()); // First // Incorrect - will fail let app = Router::new() .route("/admin", get(admin_handler)) .layer(AuthLayer::new()) // Wrong order! .layer(PermissionLayer::organization("admin:access"));
use wacht::{middleware::*, require_permission}; // Define permission at compile time require_permission!(CanManageBilling, "billing:manage", Organization); async fn billing_handler( _perm: CanManageBilling, // Handler-level check Extension(auth): Extension<AuthContext> ) -> &'static str { "Billing management" } let app = Router::new() .route("/billing", get(billing_handler)) .layer(PermissionLayer::organization("billing:read")) // Route-level check .layer(AuthLayer::new());
#[cfg(test)] mod tests { use axum::http::{Request, header, StatusCode}; use tower::ServiceExt; #[tokio::test] async fn test_permission_denied() { let app = create_app(); // User with insufficient permissions let token = create_test_token(vec!["users:read"]); let response = app .oneshot( Request::get("/admin/billing") .header(header::AUTHORIZATION, format!("Bearer {}", token)) .body(Body::empty()) .unwrap() ) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); // Check error header let error_header = response.headers() .get("X-Auth-Error") .unwrap(); assert!(error_header.to_str().unwrap().contains("billing:manage")); } }