use axum::{routing::get, Router};
use axum_acl::{AclAction, AclLayer, AclRuleFilter, AclTable, JsonDeniedHandler};
use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
const ROLE_ADMIN: u32 = 0b001;
const ROLE_USER: u32 = 0b010;
async fn public_info() -> &'static str {
"Public information - accessible to everyone"
}
async fn api_users() -> &'static str {
"API Users endpoint - requires 'user' or 'admin' role"
}
async fn api_posts() -> &'static str {
"API Posts endpoint - requires 'user' or 'admin' role"
}
async fn admin_dashboard() -> &'static str {
"Admin Dashboard - requires 'admin' role"
}
async fn admin_settings() -> &'static str {
"Admin Settings - requires 'admin' role"
}
#[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,basic=debug".into()),
)
.init();
let acl_table = AclTable::builder()
.default_action(AclAction::Deny)
.add_any(AclRuleFilter::new()
.role_mask(ROLE_ADMIN)
.action(AclAction::Allow)
.description("Admins have full access"))
.add_prefix("/api/", AclRuleFilter::new()
.role_mask(ROLE_USER)
.action(AclAction::Allow)
.description("Users can access API endpoints"))
.add_prefix("/public/", AclRuleFilter::new()
.role_mask(u32::MAX) .action(AclAction::Allow)
.description("Public endpoints accessible to all"))
.add_exact("/health", AclRuleFilter::new()
.role_mask(u32::MAX)
.action(AclAction::Allow)
.description("Health check endpoint"))
.build();
tracing::info!(
"ACL Table configured: {} exact rules, {} pattern rules",
acl_table.exact_rules().len(),
acl_table.pattern_rules().len()
);
let app = Router::new()
.route("/public/info", get(public_info))
.route("/health", get(|| async { "OK" }))
.route("/api/users", get(api_users))
.route("/api/posts", get(api_posts))
.route("/admin/dashboard", get(admin_dashboard))
.route("/admin/settings", get(admin_settings))
.layer(
AclLayer::new(acl_table)
.with_denied_handler(JsonDeniedHandler::new())
.with_anonymous_roles(0),
);
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 'X-Roles: 1' http://localhost:3000/admin/dashboard");
tracing::info!(" curl -H 'X-Roles: 2' http://localhost:3000/api/users");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}