
laye
A framework-agnostic role- and permission-based access control (RBAC) library for Rust.
Build composable AccessPolicy rules, implement Principal on your auth type, and wire
the provided middleware into actix-web or axum (tower). Authentication — decoding tokens,
querying the database — is entirely outside laye's scope.
Feature flags
| Feature |
What it adds |
actix-web |
laye::actix module: PolicyMiddlewareFactory, AuthPrincipal, MaybeAuthPrincipal |
tower |
laye::tower_middleware module: AccessControlLayer |
laye = "0.1"
laye = { version = "0.1", features = ["actix-web"] }
laye = { version = "0.1", features = ["tower"] }
How it works
- Implement
Principal on your auth info struct (decoded JWT payload, session data, etc.).
- Build an
AccessPolicy with require_all (AND) or require_any (OR), chaining AccessRules and nested sub-policies.
- Insert your principal into request extensions from your own upstream auth middleware — before
laye runs.
- Apply a
laye middleware / layer. It reads P from extensions and evaluates the policy,
returning 401 Unauthorized when no principal is found or 403 Forbidden when the principal fails the policy.
Quick start
Implement Principal
use laye::Principal;
#[derive(Clone)]
struct MyUser {
roles: Vec<String>,
permissions: Vec<String>,
authenticated: bool,
}
impl Principal for MyUser {
fn roles(&self) -> &[String] { &self.roles }
fn permissions(&self) -> &[String] { &self.permissions }
fn is_authenticated(&self) -> bool { self.authenticated }
}
Build and evaluate a policy
use laye::{AccessPolicy, AccessRule, LayeCheckResult};
let policy = AccessPolicy::require_all()
.add_rule(AccessRule::Authenticated)
.add_policy(
AccessPolicy::require_any()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Role("editor".into())),
);
let editor = MyUser { roles: vec!["editor".into()], permissions: vec![], authenticated: true };
let viewer = MyUser { roles: vec!["viewer".into()], permissions: vec![], authenticated: true };
assert_eq!(policy.check(Some(&editor)), LayeCheckResult::Authorized);
assert_eq!(policy.check(Some(&viewer)), LayeCheckResult::Forbidden);
assert_eq!(policy.check(None), LayeCheckResult::Unauthorized);
actix-web integration
laye = { version = "0.1", features = ["actix-web"] }
use actix_web::{web, App, HttpServer, HttpMessage, HttpResponse};
use laye::{AccessPolicy, AccessRule, Principal};
use laye::actix::AuthPrincipal;
#[derive(Clone)]
struct MyUser {
id: u64,
roles: Vec<String>,
permissions: Vec<String>,
}
impl Principal for MyUser {
fn roles(&self) -> &[String] { &self.roles }
fn permissions(&self) -> &[String] { &self.permissions }
fn is_authenticated(&self) -> bool { true }
}
async fn admin_handler(user: AuthPrincipal<MyUser>) -> HttpResponse {
HttpResponse::Ok().body(format!("id={}", user.0.id))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let admin_policy = AccessPolicy::require_all()
.add_rule(AccessRule::Authenticated)
.add_rule(AccessRule::Role("admin".into()));
HttpServer::new(move || {
App::new()
.wrap_fn(|req, srv| {
req.extensions_mut().insert(MyUser {
id: 1,
roles: vec!["admin".to_string()],
permissions: vec![],
});
srv.call(req)
})
.service(
web::scope("/admin")
.wrap(admin_policy.clone().into_actix_middleware::<MyUser>())
.route("", web::get().to(admin_handler)),
)
.route("/public", web::get().to(|| async { HttpResponse::Ok().body("public") }))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
axum / tower integration
laye = { version = "0.1", features = ["tower"] }
use axum::{Router, routing::get, middleware, response::IntoResponse};
use laye::{AccessPolicy, AccessRule, Principal};
#[derive(Clone)]
struct MyUser {
id: u64,
roles: Vec<String>,
permissions: Vec<String>,
}
impl Principal for MyUser {
fn roles(&self) -> &[String] { &self.roles }
fn permissions(&self) -> &[String] { &self.permissions }
fn is_authenticated(&self) -> bool { true }
}
async fn load_user(mut req: axum::extract::Request, next: middleware::Next) -> impl IntoResponse {
req.extensions_mut().insert(MyUser {
id: 1,
roles: vec!["admin".to_string()],
permissions: vec![],
});
next.run(req).await
}
async fn admin_handler() -> &'static str { "Hello, admin!" }
#[tokio::main]
async fn main() {
let policy = AccessPolicy::require_all()
.add_rule(AccessRule::Authenticated)
.add_rule(AccessRule::Role("admin".into()));
let app = Router::new()
.route(
"/admin",
get(admin_handler).layer(policy.into_tower_layer::<MyUser>()),
)
.route("/public", get(|| async { "public" }))
.layer(middleware::from_fn(load_user));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
License
MIT — see LICENSE.