1use anyhow::Result;
10use async_trait::async_trait;
11use axum::http::Request;
12use uuid::Uuid;
13
14#[derive(Debug, Clone)]
16pub enum AuthContext {
17 User {
19 user_id: Uuid,
20 tenant_id: Uuid,
21 roles: Vec<String>,
22 },
23
24 Owner {
26 user_id: Uuid,
27 tenant_id: Uuid,
28 resource_id: Uuid,
29 resource_type: String,
30 },
31
32 Service {
34 service_name: String,
35 tenant_id: Option<Uuid>,
36 },
37
38 Admin { admin_id: Uuid },
40
41 Anonymous,
43}
44
45impl AuthContext {
46 pub fn tenant_id(&self) -> Option<Uuid> {
48 match self {
49 AuthContext::User { tenant_id, .. } => Some(*tenant_id),
50 AuthContext::Owner { tenant_id, .. } => Some(*tenant_id),
51 AuthContext::Service { tenant_id, .. } => *tenant_id,
52 AuthContext::Admin { .. } => None,
53 AuthContext::Anonymous => None,
54 }
55 }
56
57 pub fn is_admin(&self) -> bool {
59 matches!(self, AuthContext::Admin { .. })
60 }
61
62 pub fn is_service(&self) -> bool {
64 matches!(self, AuthContext::Service { .. })
65 }
66
67 pub fn user_id(&self) -> Option<Uuid> {
69 match self {
70 AuthContext::User { user_id, .. } => Some(*user_id),
71 AuthContext::Owner { user_id, .. } => Some(*user_id),
72 _ => None,
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub enum AuthPolicy {
80 Public,
82
83 Authenticated,
85
86 Owner,
88
89 HasRole(Vec<String>),
91
92 ServiceOnly,
94
95 AdminOnly,
97
98 And(Vec<AuthPolicy>),
100
101 Or(Vec<AuthPolicy>),
103
104 Custom(fn(&AuthContext) -> bool),
106}
107
108impl AuthPolicy {
109 pub fn check(&self, context: &AuthContext) -> bool {
111 match self {
112 AuthPolicy::Public => true,
113
114 AuthPolicy::Authenticated => !matches!(context, AuthContext::Anonymous),
115
116 AuthPolicy::Owner => matches!(context, AuthContext::Owner { .. }),
117
118 AuthPolicy::HasRole(required_roles) => match context {
119 AuthContext::User { roles, .. } => required_roles.iter().any(|r| roles.contains(r)),
120 _ => false,
121 },
122
123 AuthPolicy::ServiceOnly => context.is_service(),
124
125 AuthPolicy::AdminOnly => context.is_admin(),
126
127 AuthPolicy::And(policies) => policies.iter().all(|p| p.check(context)),
128
129 AuthPolicy::Or(policies) => policies.iter().any(|p| p.check(context)),
130
131 AuthPolicy::Custom(f) => f(context),
132 }
133 }
134
135 pub fn parse_policy(s: &str) -> Self {
137 match s {
138 "public" => AuthPolicy::Public,
139 "authenticated" => AuthPolicy::Authenticated,
140 "owner" => AuthPolicy::Owner,
141 "service_only" => AuthPolicy::ServiceOnly,
142 "admin_only" => AuthPolicy::AdminOnly,
143 s if s.starts_with("role:") => {
144 let role = s.strip_prefix("role:").unwrap().to_string();
145 AuthPolicy::HasRole(vec![role])
146 }
147 s if s.starts_with("owner_or_role:") => {
148 let role = s.strip_prefix("owner_or_role:").unwrap().to_string();
149 AuthPolicy::Or(vec![AuthPolicy::Owner, AuthPolicy::HasRole(vec![role])])
150 }
151 _ => AuthPolicy::Authenticated, }
153 }
154}
155
156#[async_trait]
158pub trait AuthProvider: Send + Sync {
159 async fn extract_context<B>(&self, req: &Request<B>) -> Result<AuthContext>;
161
162 async fn is_owner(
164 &self,
165 user_id: &Uuid,
166 resource_id: &Uuid,
167 resource_type: &str,
168 ) -> Result<bool>;
169
170 async fn has_role(&self, user_id: &Uuid, role: &str) -> Result<bool>;
172}
173
174pub struct NoAuthProvider;
176
177#[async_trait]
178impl AuthProvider for NoAuthProvider {
179 async fn extract_context<B>(&self, _req: &Request<B>) -> Result<AuthContext> {
180 Ok(AuthContext::Anonymous)
181 }
182
183 async fn is_owner(&self, _: &Uuid, _: &Uuid, _: &str) -> Result<bool> {
184 Ok(true)
185 }
186
187 async fn has_role(&self, _: &Uuid, _: &str) -> Result<bool> {
188 Ok(false)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_policy_check() {
198 let user_context = AuthContext::User {
199 user_id: Uuid::new_v4(),
200 tenant_id: Uuid::new_v4(),
201 roles: vec!["admin".to_string()],
202 };
203
204 assert!(AuthPolicy::Authenticated.check(&user_context));
205 assert!(AuthPolicy::HasRole(vec!["admin".into()]).check(&user_context));
206 assert!(!AuthPolicy::Owner.check(&user_context));
207
208 let anon_context = AuthContext::Anonymous;
209 assert!(AuthPolicy::Public.check(&anon_context));
210 assert!(!AuthPolicy::Authenticated.check(&anon_context));
211 }
212
213 #[test]
214 fn test_policy_from_str() {
215 match AuthPolicy::parse_policy("public") {
216 AuthPolicy::Public => (),
217 _ => panic!("Expected Public"),
218 }
219
220 match AuthPolicy::parse_policy("role:admin") {
221 AuthPolicy::HasRole(roles) => assert_eq!(roles, vec!["admin"]),
222 _ => panic!("Expected HasRole"),
223 }
224 }
225}