Skip to main content

uxar/
roles.rs

1// roles.rs
2
3use axum::{extract::FromRequestParts, http::request::Parts};
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5use std::{borrow::Cow, fmt::Debug, marker::PhantomData};
6
7use crate::{Site, auth::{AuthError, AuthUser}};
8
9// Re-export the BitRole derive macro
10pub use uxar_macros::BitRole;
11
12pub type RoleType = u64;
13
14pub trait BitRole: Sized + Debug + Copy + Clone + 'static{
15
16    /// Returns the bit position (0..=63) for this role.
17    ///
18    /// Implementations should ensure the value is < 64.
19    fn role_value(self) -> u8;
20
21    /// Returns pairs of (bit_position, name) for all roles.
22    fn role_pairs() -> &'static [(u8, &'static str)];
23
24    #[inline]
25    fn role_name(self) -> Option<&'static str> {
26        for (val, name) in Self::role_pairs() {
27            if *val == self.role_value() {
28                return Some(name);
29            }
30        }
31        None
32    }
33
34    #[inline]
35    fn to_role_type(self) -> RoleType {
36        (1 as RoleType)
37            .checked_shl(self.role_value() as u32)
38            .unwrap_or(0)
39    }
40}
41
42
43pub fn format_roles<R: BitRole>(mask: RoleType) -> Vec<String> {
44    let mut roles = Vec::new();
45    for (val, name) in R::role_pairs() {
46        let Some(role_bit) = (1 as RoleType).checked_shl(*val as u32) else {
47            continue;
48        };
49        if mask & role_bit != 0 {
50            roles.push(name.to_string());
51        }
52    }
53    roles
54}
55
56
57#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58pub struct PermitAny;
59
60#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
61pub struct PermitAll;
62
63pub struct Permit<const MASK: RoleType, R: BitRole, O = PermitAny>(
64    pub AuthUser,
65    pub PhantomData<R>,
66    pub PhantomData<O>,
67);
68
69impl<const MASK: RoleType, R: BitRole> Permit<MASK, R> {
70    pub fn into_user(self) -> AuthUser {
71        self.0
72    }
73}
74
75pub trait HasPerm {
76    fn has_permission(role_mask: RoleType, perm_mask: RoleType) -> bool;
77
78    fn join_all() -> bool {
79        false
80    }
81}
82
83impl HasPerm for PermitAny {
84    fn has_permission(role_mask: RoleType, perm_mask: RoleType) -> bool {
85        role_mask & perm_mask != 0
86    }
87}
88
89impl HasPerm for PermitAll {
90    fn has_permission(role_mask: RoleType, perm_mask: RoleType) -> bool {
91        role_mask & perm_mask == perm_mask
92    }
93
94    fn join_all() -> bool {
95        true
96    }
97}
98
99impl<const MASK: RoleType, R: BitRole, O: HasPerm> FromRequestParts<Site> for Permit<MASK, R, O> {
100    type Rejection = AuthError;
101
102    async fn from_request_parts(parts: &mut Parts, site: &Site) -> Result<Self, Self::Rejection> {
103        let user = AuthUser::from_request_parts(parts, site).await?;
104        if !O::has_permission(user.roles, MASK) {
105            return Err(AuthError::Forbidden);
106        }
107        Ok(Permit(user, PhantomData, PhantomData))
108    }
109}
110
111impl<const MASK: RoleType, R: BitRole, O: HasPerm> crate::callables::IntoArgPart for Permit<MASK, R, O> {
112    fn into_arg_part() -> crate::callables::ArgPart {
113        let scopes = R::role_pairs()
114            .iter()
115            .filter_map(|(bit, name)| {
116                let role_bit = (1 as RoleType).checked_shl(*bit as u32)?;
117                (MASK & role_bit != 0).then(|| Cow::Borrowed(*name))
118            })
119            .collect();
120        crate::callables::ArgPart::Security {
121            scheme: Cow::Borrowed("bearerAuth"),
122            scopes,
123            join_all: O::join_all(),
124        }
125    }
126}
127
128
129
130#[macro_export]
131macro_rules! permit {
132    // Internal helper: role position -> mask
133    (@mask $role_ty:ty, $role:ident) => {
134        (<$role_ty>::__uxar_mask(<$role_ty>::$role))
135    };
136
137    // permit!(RoleType, Role) - single role, defaults to PermitAny
138    ($role_ty:ty, $role:ident $(,)?) => {
139        $crate::auth::Permit::<{
140            $crate::permit!(@mask $role_ty, $role)
141        }, $role_ty, $crate::auth::PermitAny>
142    };
143
144    // permit!(RoleType, Role1 & Role2 & Role3) - ALL required (PermitAll)
145    ($role_ty:ty, $first:ident $( & $rest:ident )+ $(,)?) => {
146        $crate::auth::Permit::<{
147            $crate::permit!(@mask $role_ty, $first)
148            $( | $crate::permit!(@mask $role_ty, $rest) )+
149        }, $role_ty, $crate::auth::PermitAll>
150    };
151
152    // permit!(RoleType, Role1 | Role2 | Role3) - ANY required (PermitAny)
153    ($role_ty:ty, $first:ident $( | $rest:ident )+ $(,)?) => {
154        $crate::auth::Permit::<{
155            $crate::permit!(@mask $role_ty, $first)
156            $( | $crate::permit!(@mask $role_ty, $rest) )+
157        }, $role_ty, $crate::auth::PermitAny>
158    };
159}