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