Skip to main content

axum_security/pbac/
mod.rs

1//! Policy-based access control (PBAC).
2//!
3//! Define access policies as functions, closures, or custom types that implement
4//! [`Policy<U>`]. Combine them with [`and`](PolicyExt::and), [`or`](PolicyExt::or),
5//! and [`not`](PolicyExt::not). Apply them to routes with [`PolicyRouterExt::with_policy`].
6//!
7//! Requires an active session (from the `jwt`, `cookie`, or `basic-auth` feature).
8//! Returns `401` if no session is present, `403` if the policy denies access.
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use axum::{Router, routing::get};
14//! use axum_security::pbac::{PolicyExt, PolicyRouterExt};
15//!
16//! fn is_admin(user: &MyUser) -> bool { user.admin }
17//! fn is_active(user: &MyUser) -> bool { !user.banned }
18//!
19//! let app = Router::new()
20//!     .route("/admin", get(handler).with_policy(is_admin.and(is_active)));
21//! ```
22
23use std::{convert::Infallible, future::Future, marker::PhantomData, pin::Pin};
24
25use axum::{
26    Router,
27    extract::Request,
28    http::StatusCode,
29    response::{IntoResponse, Response},
30    routing::MethodRouter,
31};
32use tower::{Layer, Service};
33
34use crate::session::Session;
35
36/// A policy that decides whether a user is allowed access.
37///
38/// Return `Ok(true)` to allow, `Ok(false)` to deny (→ 403), or `Err(e)` for a custom
39/// error response. Closures `Fn(&U) -> bool` implement this trait automatically.
40pub trait Policy<U>: Send + Sync + 'static + Clone {
41    /// The error type returned when the policy evaluation itself fails.
42    type Error: IntoResponse + Send;
43
44    /// Evaluate the policy for the given user.
45    fn evaluate(&self, user: &U) -> impl Future<Output = Result<bool, Self::Error>> + Send;
46}
47
48impl<U, F> Policy<U> for F
49where
50    F: Fn(&U) -> bool + Send + Sync + 'static + Clone,
51{
52    type Error = Infallible;
53
54    fn evaluate(&self, user: &U) -> impl Future<Output = Result<bool, Self::Error>> + Send {
55        let result = (self)(user);
56        async move { Ok(result) }
57    }
58}
59
60/// Combinator: both policies must allow. Created by [`PolicyExt::and`].
61#[derive(Clone)]
62pub struct AllOf<P1, P2>(pub P1, pub P2);
63
64impl<U, P1, P2> Policy<U> for AllOf<P1, P2>
65where
66    U: Send + Sync + 'static,
67    P1: Policy<U>,
68    P2: Policy<U>,
69{
70    type Error = Response;
71
72    async fn evaluate(&self, user: &U) -> Result<bool, Self::Error> {
73        let a = self
74            .0
75            .evaluate(user)
76            .await
77            .map_err(IntoResponse::into_response)?;
78        if !a {
79            return Ok(false);
80        }
81        self.1
82            .evaluate(user)
83            .await
84            .map_err(IntoResponse::into_response)
85    }
86}
87
88/// Combinator: at least one policy must allow. Created by [`PolicyExt::or`].
89#[derive(Clone)]
90pub struct AnyOf<P1, P2>(pub P1, pub P2);
91
92impl<U, P1, P2> Policy<U> for AnyOf<P1, P2>
93where
94    U: Send + Sync + 'static,
95    P1: Policy<U>,
96    P2: Policy<U>,
97{
98    type Error = Response;
99
100    async fn evaluate(&self, user: &U) -> Result<bool, Self::Error> {
101        let a = self
102            .0
103            .evaluate(user)
104            .await
105            .map_err(IntoResponse::into_response)?;
106        if a {
107            return Ok(true);
108        }
109        self.1
110            .evaluate(user)
111            .await
112            .map_err(IntoResponse::into_response)
113    }
114}
115
116/// Combinator: inverts the policy. Created by [`PolicyExt::not`].
117#[derive(Clone)]
118pub struct Not<P>(pub P);
119
120impl<U, P> Policy<U> for Not<P>
121where
122    U: Send + Sync + 'static,
123    P: Policy<U>,
124{
125    type Error = P::Error;
126
127    async fn evaluate(&self, user: &U) -> Result<bool, Self::Error> {
128        Ok(!self.0.evaluate(user).await?)
129    }
130}
131
132/// Provides [`and`](PolicyExt::and), [`or`](PolicyExt::or), and [`not`](PolicyExt::not) combinators.
133///
134/// Automatically implemented for all [`Policy`] types.
135pub trait PolicyExt<U>: Policy<U> + Sized {
136    /// Require both `self` and `other` to allow access.
137    fn and<P: Policy<U>>(self, other: P) -> AllOf<Self, P> {
138        AllOf(self, other)
139    }
140
141    /// Allow access if either `self` or `other` allows.
142    fn or<P: Policy<U>>(self, other: P) -> AnyOf<Self, P> {
143        AnyOf(self, other)
144    }
145
146    /// Invert this policy (allow becomes deny and vice versa).
147    fn not(self) -> Not<Self> {
148        Not(self)
149    }
150}
151
152impl<U, P: Policy<U>> PolicyExt<U> for P {}
153
154/// A policy that always allows access.
155#[derive(Clone, Copy)]
156pub struct Allow;
157
158impl<U: Send + Sync + 'static> Policy<U> for Allow {
159    type Error = Infallible;
160
161    async fn evaluate(&self, _user: &U) -> Result<bool, Self::Error> {
162        Ok(true)
163    }
164}
165
166/// A policy that always denies access.
167#[derive(Clone, Copy)]
168pub struct Deny;
169
170impl<U: Send + Sync + 'static> Policy<U> for Deny {
171    type Error = Infallible;
172
173    async fn evaluate(&self, _user: &U) -> Result<bool, Self::Error> {
174        Ok(false)
175    }
176}
177
178/// Bridge from RBAC: a policy that checks if the user has a specific role.
179///
180/// Requires the `rbac` feature.
181#[cfg(feature = "rbac")]
182pub struct HasRole<R: crate::rbac::RBAC> {
183    role: R,
184}
185
186#[cfg(feature = "rbac")]
187impl<R: crate::rbac::RBAC> Clone for HasRole<R> {
188    fn clone(&self) -> Self {
189        HasRole { role: self.role }
190    }
191}
192
193#[cfg(feature = "rbac")]
194impl<R: crate::rbac::RBAC> HasRole<R> {
195    pub fn new(role: R) -> Self {
196        HasRole { role }
197    }
198}
199
200#[cfg(feature = "rbac")]
201impl<R: crate::rbac::RBAC> Policy<R::Resource> for HasRole<R> {
202    type Error = Infallible;
203
204    async fn evaluate(&self, user: &R::Resource) -> Result<bool, Self::Error> {
205        Ok(R::extract_roles(user).into_iter().any(|r| *r == self.role))
206    }
207}
208
209struct PolicyLayer<P, U> {
210    policy: P,
211    _marker: PhantomData<fn() -> U>,
212}
213
214impl<P: Clone, U> Clone for PolicyLayer<P, U> {
215    fn clone(&self) -> Self {
216        PolicyLayer {
217            policy: self.policy.clone(),
218            _marker: PhantomData,
219        }
220    }
221}
222
223impl<P, S, U> Layer<S> for PolicyLayer<P, U>
224where
225    P: Clone,
226{
227    type Service = PolicyService<P, S, U>;
228
229    fn layer(&self, inner: S) -> PolicyService<P, S, U> {
230        PolicyService {
231            policy: self.policy.clone(),
232            inner,
233            _marker: PhantomData,
234        }
235    }
236}
237
238/// The [`Service`] created by `PolicyLayer`. You don't need to construct this directly.
239pub struct PolicyService<P, S, U> {
240    policy: P,
241    inner: S,
242    _marker: PhantomData<fn() -> U>,
243}
244
245impl<P: Clone, S: Clone, U> Clone for PolicyService<P, S, U> {
246    fn clone(&self) -> Self {
247        PolicyService {
248            policy: self.policy.clone(),
249            inner: self.inner.clone(),
250            _marker: PhantomData,
251        }
252    }
253}
254
255type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
256
257impl<P, S, U> Service<Request> for PolicyService<P, S, U>
258where
259    P: Policy<U> + 'static,
260    U: Clone + Send + Sync + 'static,
261    S: Service<Request, Response = Response> + Clone + Send + 'static,
262    S::Error: Send,
263    S::Future: Send,
264{
265    type Response = Response;
266    type Error = S::Error;
267    type Future = BoxFuture<Result<Response, S::Error>>;
268
269    fn poll_ready(
270        &mut self,
271        cx: &mut std::task::Context<'_>,
272    ) -> std::task::Poll<Result<(), Self::Error>> {
273        self.inner.poll_ready(cx)
274    }
275
276    fn call(&mut self, mut req: Request) -> Self::Future {
277        let policy = self.policy.clone();
278        let mut inner = self.inner.clone();
279        Box::pin(async move {
280            crate::debug!("evaluating policy");
281
282            let Some(session) = Session::<U>::from_extensions(req.extensions_mut()) else {
283                crate::debug!("no session found, returning 401");
284                return Ok(StatusCode::UNAUTHORIZED.into_response());
285            };
286
287            match policy.evaluate(&session).await {
288                Ok(true) => {
289                    session.insert_into(req.extensions_mut());
290                    inner.call(req).await
291                }
292                Ok(false) => {
293                    crate::debug!("policy denied access, returning 403");
294                    session.insert_into(req.extensions_mut());
295                    Ok(StatusCode::FORBIDDEN.into_response())
296                }
297                Err(e) => {
298                    crate::debug!("policy returned error");
299                    session.insert_into(req.extensions_mut());
300                    Ok(e.into_response())
301                }
302            }
303        })
304    }
305}
306
307/// Extension trait for applying a [`Policy`] to a [`MethodRouter`] or [`Router`].
308pub trait PolicyRouterExt {
309    /// Apply a policy to this route. Denies with `403` if the policy returns `false`.
310    fn with_policy<P, U>(self, policy: P) -> Self
311    where
312        P: Policy<U> + 'static,
313        U: Clone + Send + Sync + 'static;
314}
315
316impl<S: Clone + 'static> PolicyRouterExt for MethodRouter<S, Infallible> {
317    fn with_policy<P, U>(self, policy: P) -> Self
318    where
319        P: Policy<U> + 'static,
320        U: Clone + Send + Sync + 'static,
321    {
322        self.layer(PolicyLayer {
323            policy,
324            _marker: PhantomData,
325        })
326    }
327}
328
329impl<S: Clone + Send + Sync + 'static> PolicyRouterExt for Router<S> {
330    fn with_policy<P, U>(self, policy: P) -> Self
331    where
332        P: Policy<U> + 'static,
333        U: Clone + Send + Sync + 'static,
334    {
335        self.layer(PolicyLayer {
336            policy,
337            _marker: PhantomData,
338        })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[derive(Clone)]
347    struct User {
348        admin: bool,
349        banned: bool,
350    }
351
352    fn is_admin(u: &User) -> bool {
353        u.admin
354    }
355
356    fn is_not_banned(u: &User) -> bool {
357        !u.banned
358    }
359
360    #[tokio::test]
361    async fn closure_policy_passes() {
362        let user = User {
363            admin: true,
364            banned: false,
365        };
366        assert!(is_admin.evaluate(&user).await.unwrap());
367    }
368
369    #[tokio::test]
370    async fn closure_policy_fails() {
371        let user = User {
372            admin: false,
373            banned: false,
374        };
375        assert!(!is_admin.evaluate(&user).await.unwrap());
376    }
377
378    #[tokio::test]
379    async fn allow_always_passes() {
380        let user = User {
381            admin: false,
382            banned: true,
383        };
384        assert!(Allow.evaluate(&user).await.unwrap());
385    }
386
387    #[tokio::test]
388    async fn deny_always_fails() {
389        let user = User {
390            admin: true,
391            banned: false,
392        };
393        assert!(!Deny.evaluate(&user).await.unwrap());
394    }
395
396    #[tokio::test]
397    async fn not_combinator() {
398        let user = User {
399            admin: true,
400            banned: false,
401        };
402        let policy = is_admin.not();
403        assert!(!policy.evaluate(&user).await.unwrap());
404    }
405
406    #[tokio::test]
407    async fn all_of_both_pass() {
408        let user = User {
409            admin: true,
410            banned: false,
411        };
412        let policy = is_admin.and(is_not_banned);
413        assert!(policy.evaluate(&user).await.unwrap());
414    }
415
416    #[tokio::test]
417    async fn all_of_one_fails() {
418        let user = User {
419            admin: true,
420            banned: true,
421        };
422        let policy = is_admin.and(is_not_banned);
423        assert!(!policy.evaluate(&user).await.unwrap());
424    }
425
426    #[tokio::test]
427    async fn any_of_one_passes() {
428        let user = User {
429            admin: true,
430            banned: true,
431        };
432        let policy = is_admin.or(is_not_banned);
433        assert!(policy.evaluate(&user).await.unwrap());
434    }
435
436    #[tokio::test]
437    async fn any_of_none_pass() {
438        let user = User {
439            admin: false,
440            banned: true,
441        };
442        let policy = is_admin.or(is_not_banned);
443        assert!(!policy.evaluate(&user).await.unwrap());
444    }
445
446    #[tokio::test]
447    async fn chained_combinators() {
448        let user = User {
449            admin: true,
450            banned: false,
451        };
452        let policy = is_admin.and(is_not_banned);
453        assert!(policy.evaluate(&user).await.unwrap());
454
455        let policy2 = (is_admin as fn(&User) -> bool).not().or(is_not_banned);
456        assert!(policy2.evaluate(&user).await.unwrap());
457    }
458}