axum_acl/
lib.rs

1//! # axum-acl
2//!
3//! Flexible Access Control List (ACL) middleware for [axum](https://docs.rs/axum) 0.8.
4//!
5//! This crate provides a configurable ACL system using a 5-tuple rule system:
6//! - **Endpoint**: Request path (HashMap key for O(1) lookup, or prefix/glob patterns)
7//! - **Role**: `u32` bitmask for up to 32 roles per user
8//! - **Time**: Time windows when rules are active (business hours, weekdays, etc.)
9//! - **IP**: Client IP address (single IP, CIDR ranges, or lists)
10//! - **ID**: User/session ID matching (exact or wildcard)
11//!
12//! ## Features
13//!
14//! - **Fast endpoint lookup** - HashMap for exact matches (O(1)), patterns for prefix/glob
15//! - **Efficient role matching** - `u32` bitmask with single AND operation
16//! - **Pluggable role extraction** - Headers, extensions, or custom extractors
17//! - **Time-based access control** - Business hours, specific days
18//! - **IP-based filtering** - Single IP, CIDR notation, lists
19//! - **ID matching** - Exact match or wildcard for user/session IDs
20//!
21//! ## Quick Start
22//!
23//! ```no_run
24//! use axum::{Router, routing::get};
25//! use axum_acl::{AclLayer, AclTable, AclRuleFilter, AclAction, TimeWindow};
26//! use std::net::SocketAddr;
27//!
28//! // Define role bits
29//! const ROLE_ADMIN: u32 = 0b001;
30//! const ROLE_USER: u32 = 0b010;
31//!
32//! async fn public_handler() -> &'static str {
33//!     "Public content"
34//! }
35//!
36//! async fn admin_handler() -> &'static str {
37//!     "Admin only"
38//! }
39//!
40//! #[tokio::main]
41//! async fn main() {
42//!     // Define ACL rules
43//!     let acl_table = AclTable::builder()
44//!         // Default action when no rules match
45//!         .default_action(AclAction::Deny)
46//!         // Allow admins to access everything
47//!         .add_any(AclRuleFilter::new()
48//!             .role_mask(ROLE_ADMIN)
49//!             .action(AclAction::Allow)
50//!             .description("Admins can access everything"))
51//!         // Allow users to access /api/** during business hours
52//!         .add_prefix("/api/", AclRuleFilter::new()
53//!             .role_mask(ROLE_USER)
54//!             .time(TimeWindow::hours_on_days(9, 17, vec![0, 1, 2, 3, 4])) // Mon-Fri 9-5
55//!             .action(AclAction::Allow)
56//!             .description("Users can access API during business hours"))
57//!         // Allow anyone to access /public/** (all roles)
58//!         .add_prefix("/public/", AclRuleFilter::new()
59//!             .role_mask(u32::MAX)
60//!             .action(AclAction::Allow)
61//!             .description("Public endpoints"))
62//!         .build();
63//!
64//!     // Build the router with ACL middleware
65//!     let app = Router::new()
66//!         .route("/public/info", get(public_handler))
67//!         .route("/admin/dashboard", get(admin_handler))
68//!         .layer(AclLayer::new(acl_table));
69//!
70//!     // Important: Use into_make_service_with_connect_info for IP extraction
71//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
72//!     axum::serve(
73//!         listener,
74//!         app.into_make_service_with_connect_info::<SocketAddr>()
75//!     ).await.unwrap();
76//! }
77//! ```
78//!
79//! ## Rule Evaluation
80//!
81//! 1. **Endpoint lookup**: Exact matches checked first (O(1) HashMap), then patterns
82//! 2. **Filter matching**: For each endpoint match, filters are checked in order:
83//!    - ID match: `rule.id == "*"` OR `rule.id == ctx.id`
84//!    - Role match: `(rule.role_mask & ctx.roles) != 0`
85//!    - IP match: CIDR-style network matching
86//!    - Time match: Current time within window
87//!
88//! First matching rule determines the action. Default action used if no match.
89//!
90//! ## Role Extraction
91//!
92//! By default, roles are extracted from the `X-Roles` header as a `u32` bitmask.
93//! The header can contain decimal (e.g., `5`) or hex (e.g., `0x1F`) values.
94//!
95//! ```no_run
96//! use axum_acl::{AclLayer, AclTable, HeaderRoleExtractor};
97//!
98//! let table = AclTable::new();
99//!
100//! // Use a different header with default roles for anonymous users
101//! let layer = AclLayer::new(table)
102//!     .with_extractor(HeaderRoleExtractor::new("X-User-Roles").with_default_roles(0b100));
103//! ```
104//!
105//! For more complex scenarios, implement the [`RoleExtractor`] trait:
106//!
107//! ```
108//! use axum_acl::{RoleExtractor, RoleExtractionResult};
109//! use http::Request;
110//!
111//! const ROLE_ADMIN: u32 = 0b001;
112//! const ROLE_USER: u32 = 0b010;
113//!
114//! struct JwtRoleExtractor;
115//!
116//! impl<B> RoleExtractor<B> for JwtRoleExtractor {
117//!     fn extract_roles(&self, request: &Request<B>) -> RoleExtractionResult {
118//!         // Extract and validate JWT, return the roles bitmask
119//!         if let Some(auth) = request.headers().get("Authorization") {
120//!             // Parse JWT and extract roles...
121//!             RoleExtractionResult::Roles(ROLE_USER)
122//!         } else {
123//!             RoleExtractionResult::Anonymous
124//!         }
125//!     }
126//! }
127//! ```
128//!
129//! ## Endpoint Patterns
130//!
131//! - **Exact**: `EndpointPattern::exact("/api/users")` - matches only `/api/users`
132//! - **Prefix**: `EndpointPattern::prefix("/api/")` - matches `/api/users`, `/api/posts`, etc.
133//! - **Glob**: `EndpointPattern::glob("/api/*/users")` - matches `/api/v1/users`, `/api/v2/users`
134//!   - `*` matches exactly one path segment
135//!   - `**` matches zero or more path segments
136//! - **Any**: `EndpointPattern::any()` - matches all paths
137//!
138//! ## Time Windows
139//!
140//! ```
141//! use axum_acl::TimeWindow;
142//!
143//! // Any time (default)
144//! let always = TimeWindow::any();
145//!
146//! // 9 AM to 5 PM UTC
147//! let business_hours = TimeWindow::hours(9, 17);
148//!
149//! // Monday to Friday, 9 AM to 5 PM UTC
150//! let weekday_hours = TimeWindow::hours_on_days(9, 17, vec![0, 1, 2, 3, 4]);
151//! ```
152//!
153//! ## IP Matching
154//!
155//! ```
156//! use axum_acl::IpMatcher;
157//!
158//! // Any IP
159//! let any = IpMatcher::any();
160//!
161//! // Single IP
162//! let single = IpMatcher::parse("192.168.1.1").unwrap();
163//!
164//! // CIDR range
165//! let network = IpMatcher::parse("10.0.0.0/8").unwrap();
166//! ```
167//!
168//! ## Behind a Reverse Proxy
169//!
170//! When running behind a reverse proxy, configure the middleware to read the
171//! client IP from a header:
172//!
173//! ```no_run
174//! use axum_acl::{AclLayer, AclTable};
175//!
176//! let table = AclTable::new();
177//! let layer = AclLayer::new(table)
178//!     .with_forwarded_ip_header("X-Forwarded-For");
179//! ```
180//!
181//! ## Custom Denied Response
182//!
183//! ```
184//! use axum_acl::{AclLayer, AclTable, AccessDeniedHandler, AccessDenied, JsonDeniedHandler};
185//! use axum::response::{Response, IntoResponse};
186//! use http::StatusCode;
187//!
188//! // Use the built-in JSON handler
189//! let layer = AclLayer::new(AclTable::new())
190//!     .with_denied_handler(JsonDeniedHandler::new());
191//!
192//! // Or implement your own
193//! struct CustomHandler;
194//!
195//! impl AccessDeniedHandler for CustomHandler {
196//!     fn handle(&self, denied: &AccessDenied) -> Response {
197//!         (StatusCode::FORBIDDEN, "Custom denied message").into_response()
198//!     }
199//! }
200//! ```
201//!
202//! ## Dynamic Rules
203//!
204//! Implement [`AclRuleProvider`] to load rules from external sources:
205//!
206//! ```
207//! use axum_acl::{AclRuleProvider, RuleEntry, AclRuleFilter, AclTable, AclAction, EndpointPattern};
208//!
209//! const ROLE_ADMIN: u32 = 0b001;
210//!
211//! struct DatabaseRuleProvider {
212//!     // connection pool, etc.
213//! }
214//!
215//! impl DatabaseRuleProvider {
216//!     fn load_rules(&self) -> Result<Vec<RuleEntry>, std::io::Error> {
217//!         // Query database for rules
218//!         Ok(vec![
219//!             RuleEntry::any(AclRuleFilter::new()
220//!                 .role_mask(ROLE_ADMIN)
221//!                 .action(AclAction::Allow))
222//!         ])
223//!     }
224//! }
225//!
226//! // Use with the table builder
227//! fn build_table(provider: &DatabaseRuleProvider) -> AclTable {
228//!     let rules = provider.load_rules().unwrap();
229//!     let mut builder = AclTable::builder();
230//!     for entry in rules {
231//!         builder = builder.add_pattern(entry.pattern, entry.filter);
232//!     }
233//!     builder.build()
234//! }
235//! ```
236
237#![warn(missing_docs)]
238#![warn(rustdoc::missing_crate_level_docs)]
239#![forbid(unsafe_code)]
240
241/// Role bitmask constant for anonymous/unauthenticated users.
242///
243/// This constant uses bit 31 (0x80000000) as a convention for anonymous access.
244/// Bits 0-30 remain available for application-defined roles.
245///
246/// # Usage
247///
248/// Use this with `HeaderRoleExtractor::with_default_roles()` to allow
249/// anonymous users to match public endpoint rules:
250///
251/// ```rust
252/// use axum_acl::{AclLayer, AclTable, AclRuleFilter, AclAction, HeaderRoleExtractor, ROLE_ANONYMOUS};
253///
254/// let table = AclTable::builder()
255///     .default_action(AclAction::Deny)
256///     // Public endpoints: allow all roles including anonymous
257///     .add_prefix("/public/", AclRuleFilter::new()
258///         .role_mask(u32::MAX)  // All roles including ROLE_ANONYMOUS
259///         .action(AclAction::Allow))
260///     .build();
261///
262/// let layer = AclLayer::new(table)
263///     .with_extractor(HeaderRoleExtractor::new("X-Roles")
264///         .with_default_roles(ROLE_ANONYMOUS));
265/// ```
266pub const ROLE_ANONYMOUS: u32 = 0x8000_0000;
267
268mod config;
269mod error;
270mod extractor;
271mod middleware;
272mod rule;
273mod table;
274
275// Re-export main types
276pub use config::{AclConfig as TomlConfig, ConfigError, ConfigSettings, RuleConfig};
277pub use error::{AccessDenied, AccessDeniedHandler, AclError, DefaultDeniedHandler, JsonDeniedHandler};
278pub use extractor::{
279    // Role extraction
280    AnonymousRoleExtractor, ChainedRoleExtractor, ExtensionRoleExtractor, FixedRoleExtractor,
281    HeaderRoleExtractor, RoleExtractionResult, RoleExtractor,
282    // ID extraction
283    AnonymousIdExtractor, ExtensionIdExtractor, FixedIdExtractor, HeaderIdExtractor,
284    IdExtractionResult, IdExtractor,
285};
286pub use middleware::{AclConfig, AclLayer, AclMiddleware};
287pub use rule::{AclAction, AclRuleFilter, EndpointPattern, IpMatcher, RequestContext, TimeWindow};
288pub use table::{AclRuleProvider, AclTable, AclTableBuilder, RuleEntry, StaticRuleProvider};
289
290/// Prelude module for convenient imports.
291///
292/// ```
293/// use axum_acl::prelude::*;
294/// ```
295pub mod prelude {
296    pub use crate::config::ConfigError;
297    pub use crate::error::{AccessDenied, AccessDeniedHandler, AclError};
298    pub use crate::extractor::{
299        HeaderRoleExtractor, RoleExtractionResult, RoleExtractor,
300        HeaderIdExtractor, IdExtractionResult, IdExtractor,
301    };
302    pub use crate::middleware::AclLayer;
303    pub use crate::rule::{AclAction, AclRuleFilter, EndpointPattern, IpMatcher, RequestContext, TimeWindow};
304    pub use crate::table::{AclRuleProvider, AclTable, RuleEntry};
305    pub use crate::ROLE_ANONYMOUS;
306}