use axum::{
extract::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router,
};
use axum_acl::{
AclAction, AclLayer, AclRuleFilter, AclTable, IpMatcher, RoleExtractionResult,
RoleExtractor, TimeWindow,
};
use http::StatusCode;
use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
const ROLE_ADMIN: u32 = 0b001;
const ROLE_USER: u32 = 0b010;
#[derive(Clone, Debug)]
struct AuthenticatedUser {
id: String,
roles: u32, }
async fn auth_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
if let Some(auth_header) = request.headers().get("Authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(role_name) = auth_str.strip_prefix("Bearer ") {
let roles = match role_name {
"admin" => ROLE_ADMIN,
"user" => ROLE_USER,
"both" => ROLE_ADMIN | ROLE_USER,
_ => 0,
};
let user = AuthenticatedUser {
id: format!("{}-123", role_name),
roles,
};
request.extensions_mut().insert(user);
}
}
}
Ok(next.run(request).await)
}
#[derive(Clone)]
struct AuthUserRoleExtractor;
impl<B> RoleExtractor<B> for AuthUserRoleExtractor {
fn extract_roles(&self, request: &http::Request<B>) -> RoleExtractionResult {
match request.extensions().get::<AuthenticatedUser>() {
Some(user) => {
tracing::debug!(user_id = %user.id, roles = user.roles, "Extracted roles from auth");
RoleExtractionResult::Roles(user.roles)
}
None => {
tracing::debug!("No authenticated user found");
RoleExtractionResult::Anonymous
}
}
}
}
async fn public_endpoint() -> &'static str {
"Public - no auth required"
}
async fn user_endpoint() -> &'static str {
"User endpoint - requires 'user' or 'admin' role"
}
async fn admin_endpoint() -> &'static str {
"Admin endpoint - requires 'admin' role"
}
async fn business_hours_endpoint() -> &'static str {
"Business hours only endpoint"
}
async fn internal_endpoint() -> &'static str {
"Internal endpoint - localhost only"
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "axum_acl=debug,custom_extractor=debug".into()),
)
.init();
let acl_table = AclTable::builder()
.default_action(AclAction::Deny)
.add_any(AclRuleFilter::new()
.role_mask(ROLE_ADMIN)
.action(AclAction::Allow)
.description("Admin full access"))
.add_prefix("/internal/", AclRuleFilter::new()
.role_mask(u32::MAX) .ip(IpMatcher::parse("127.0.0.1").unwrap())
.action(AclAction::Allow)
.description("Internal endpoints - localhost only"))
.add_prefix("/internal/", AclRuleFilter::new()
.role_mask(u32::MAX)
.action(AclAction::Deny)
.description("Block internal from external IPs"))
.add_exact("/business", AclRuleFilter::new()
.role_mask(ROLE_USER)
.time(TimeWindow::hours_on_days(9, 17, vec![0, 1, 2, 3, 4]))
.action(AclAction::Allow)
.description("Business hours access"))
.add_prefix("/user/", AclRuleFilter::new()
.role_mask(ROLE_USER)
.action(AclAction::Allow)
.description("User endpoints"))
.add_prefix("/public/", AclRuleFilter::new()
.role_mask(u32::MAX)
.action(AclAction::Allow)
.description("Public access"))
.build();
tracing::info!(
"Configured ACL: {} exact rules, {} pattern rules",
acl_table.exact_rules().len(),
acl_table.pattern_rules().len()
);
let app = Router::new()
.route("/public/info", get(public_endpoint))
.route("/user/profile", get(user_endpoint))
.route("/admin/settings", get(admin_endpoint))
.route("/business", get(business_hours_endpoint))
.route("/internal/metrics", get(internal_endpoint))
.layer(AclLayer::new(acl_table).with_role_extractor(AuthUserRoleExtractor))
.layer(middleware::from_fn(auth_middleware));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("Starting server on {}", addr);
tracing::info!("Test with:");
tracing::info!(" curl http://localhost:3000/public/info");
tracing::info!(" curl -H 'Authorization: Bearer user' http://localhost:3000/user/profile");
tracing::info!(" curl -H 'Authorization: Bearer admin' http://localhost:3000/admin/settings");
tracing::info!(" curl http://localhost:3000/internal/metrics");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}