axum-acl
Flexible Access Control List (ACL) middleware for axum 0.8.
Features
- TOML Configuration - Define rules in config files (compile-time or startup)
- Table-based rules - No hardcoded rules; all access control is configured at startup
- Five-tuple matching - Rules match on Endpoint + Role + ID + IP + Time
- Extended actions - Allow, Deny, Error (custom codes), Reroute, Log
- Flexible extractors - Extract roles (u32 bitmask) and IDs from headers, extensions, or custom sources
- Path parameters - Match
{id}in paths against user ID for ownership-based access - Pattern matching - Exact, prefix, and glob patterns for endpoints
- Time windows - Restrict access to specific hours or days
- IP filtering - Single IPs, CIDR ranges, or lists
- Priority ordering - Control rule evaluation order via priority field
Installation
Add to your Cargo.toml:
[]
= "0.1"
= "0.8"
= { = "1", = ["full"] }
Quick Start
use ;
use ;
use SocketAddr;
// Define role bits
const ROLE_ADMIN: u32 = 0b001;
const ROLE_USER: u32 = 0b010;
async
Test it:
# Public endpoint (allowed)
# API as user (role_mask=2, allowed)
# Admin as user (denied)
# Admin as admin (role_mask=1, allowed)
TOML Configuration
Define rules in TOML format - either embedded at compile-time or loaded from a file at startup.
Compile-time Embedded Config
use AclTable;
// Embed configuration at compile time
const CONFIG: &str = include_str!;
Startup File Loading
use AclTable;
TOML Format
[]
= "deny"
# Rules are sorted by priority (lower = higher priority)
[[]]
= "*"
= 1 # Admin role bit
= "allow"
= 10
= "Admins have full access"
[[]]
= "/api/**"
= 2 # User role bit
= { = 9, = 17, = [0,1,2,3,4] } # Mon-Fri 9-5 UTC
= "allow"
= 100
[[]]
= "/admin/**"
= "*" # Any role
= { = "error", = 403, = "Admin access required" }
= 20
[[]]
= "/boat/{id}/**"
= 2
= "{id}" # Match path {id} against user ID
= "allow"
= 50
[[]]
= "/internal/**"
= "*"
= "127.0.0.1"
= "allow"
= 50
[[]]
= "/public/**"
= "*"
= "allow"
= 200
Action Types
| Action | TOML Syntax | Description |
|---|---|---|
| Allow | "allow" |
Allow the request |
| Deny | "deny" or "block" |
Return 403 Forbidden |
| Error | { type = "error", code = 418, message = "..." } |
Custom HTTP error |
| Reroute | { type = "reroute", target = "/path" } |
Redirect to another path |
| Log | { type = "log", level = "warn", message = "..." } |
Log and allow |
Rule Structure (5-Tuple)
Each rule matches on five dimensions:
| Field | Description | Default |
|---|---|---|
endpoint |
Path pattern to match | Any |
role_mask |
u32 bitmask or * for any |
Required |
id |
User ID match: exact, *, or {id} for path param |
* (any) |
ip |
Client IP(s) to match | Any IP |
time |
Time window when rule is active | Any time |
action |
Allow, Deny, Error, Reroute | Allow |
Rules are evaluated in order. The first matching rule wins.
Matching Logic
endpoint: HashMap lookup (O(1) for exact) or pattern match
role: (rule.role_mask & request.roles) != 0
id: rule.id == "*" OR rule.id == request.id OR rule.id == "{id}" (path param)
ip: CIDR match (ip & mask == network)
time: start <= now <= end AND day in days
Endpoint Patterns
use EndpointPattern;
// Match any path
any
// Exact match
exact // Only /api/users
// Prefix match
prefix // /api/*, /api/users, etc.
// Glob patterns
glob // /api/v1/users, /api/v2/users
glob // /api/export, /api/v1/data/export
// Path parameters (matched against user ID)
glob // {id} matches user's ID
// Parse from string
parse // Prefix (ends with /)
parse // Exact
parse // Glob
parse // Any
Role Extraction
Roles are extracted as a u32 bitmask, allowing up to 32 roles per user.
Default: Header as Bitmask
// X-Roles header parsed as decimal or hex
// X-Roles: 5 -> 0b101 (roles 0 and 2)
// X-Roles: 0x1F -> 0b11111 (roles 0-4)
Custom Header
use ;
let layer = new
.with_extractor;
With Default Roles for Anonymous Users
use HeaderRoleExtractor;
const ROLE_GUEST: u32 = 0b100;
let extractor = new
.with_default_roles;
Custom Role Translation
Translate your role scheme (strings, enums, etc.) to u32 bitmask:
use ;
use Request;
// Your role definitions
const ROLE_ADMIN: u32 = 1 << 0;
const ROLE_USER: u32 = 1 << 1;
const ROLE_GUEST: u32 = 1 << 2;
;
let layer = new
.with_extractor;
ID Extraction
User IDs are extracted as strings for matching against {id} path parameters.
Header-based ID
use HeaderIdExtractor;
let layer = new
.with_id_extractor;
Custom ID Extraction
use ;
use Request;
;
Path Parameter Matching
Match {id} in paths against the user's ID for ownership-based access:
use ;
const ROLE_USER: u32 = 0b010;
let table = builder
.default_action
// Users can only access their own boat data
.add_pattern
.build;
// User with id="boat-123":
// /api/boat/boat-123/details -> ALLOWED
// /api/boat/boat-456/details -> DENIED
Time Windows
use TimeWindow;
// Any time (default)
any
// Specific hours (UTC)
hours // 9 AM - 5 PM UTC
// Specific hours on specific days
hours_on_days
IP Matching
use IpMatcher;
// Any IP (default)
any
// Single IP
single
// CIDR range
cidr
// Parse from string
parse.unwrap // Any
parse.unwrap // Single
parse.unwrap // CIDR
Behind a Reverse Proxy
When behind nginx, traefik, or similar:
let layer = new
.with_forwarded_ip_header;
Custom Denied Response
use ;
use ;
use StatusCode;
// Use built-in JSON handler
let layer = new
.with_denied_handler;
// Or custom handler
;
Dynamic Rules from Database
use ;
// Usage at startup
Endpoint Parser Tool
Discover endpoints and their ACL rules from your codebase:
# Build the parser
# Parse endpoints (table format)
# Output as CSV
# Output as TOML config
# Use AST-based parsing (more accurate, requires feature)
CLI Arguments
Usage: endpoint_parser [OPTIONS] <directory>
Options:
--text Use text-based parsing (default, fast)
--ast Use AST-based parsing (requires --features ast-parser)
--table Output as formatted table (default)
--csv Output as CSV
--toml Output as TOML config file
--help Show help message
Output Format
ENDPOINT METHOD ROLE, ID, IP, TIME | ACTION HANDLER LOCATION
------------------------------------------------------------------------------------------------------------------------
/admin/dashboard GET ROLE_ADMIN, *, *, * | allow admin_dashboard basic.rs:109
/api/users GET ROLE_USER, *, *, * | allow api_users basic.rs:106
/public/info GET *, *, *, * | allow public_info basic.rs:103
API Reference
Core Types
| Type | Description |
|---|---|
AclTable |
Container for ACL rules (HashMap + patterns) |
AclRuleFilter |
Filter for 5-tuple matching (role, id, ip, time, action) |
AclAction |
Allow, Deny, Error, Reroute, Log |
EndpointPattern |
Path matching: Exact, Prefix, Glob, Any |
RequestContext |
Request metadata: roles (u32), ip, id |
TimeWindow |
Time-based restriction |
IpMatcher |
IP address matching |
Middleware
| Type | Description |
|---|---|
AclLayer |
Tower layer for adding ACL to router |
AclMiddleware |
The middleware service |
AclConfig |
Middleware configuration |
Role Extraction
| Type | Description |
|---|---|
RoleExtractor |
Trait for extracting roles (u32 bitmask) |
HeaderRoleExtractor |
Extract from HTTP header |
ExtensionRoleExtractor |
Extract from request extension |
FixedRoleExtractor |
Always returns same roles |
ChainedRoleExtractor |
Try multiple extractors |
ID Extraction
| Type | Description |
|---|---|
IdExtractor |
Trait for extracting user ID (String) |
HeaderIdExtractor |
Extract from HTTP header |
ExtensionIdExtractor |
Extract from request extension |
FixedIdExtractor |
Always returns same ID |
Error Handling
| Type | Description |
|---|---|
AccessDenied |
Access denied error |
AccessDeniedHandler |
Trait for custom responses |
DefaultDeniedHandler |
Plain text 403 response |
JsonDeniedHandler |
JSON 403 response |
Examples
Run the examples:
# Basic usage with builder API
# Custom role extraction from request extensions
# TOML configuration (compile-time and startup)
License
MIT OR Apache-2.0