this/core/
auth.rs

1//! Authorization system for This-RS
2//!
3//! Provides context-based authorization with multiple auth types:
4//! - User authentication
5//! - Owner-based access
6//! - Service-to-service
7//! - Admin access
8
9use anyhow::Result;
10use async_trait::async_trait;
11use axum::http::Request;
12use uuid::Uuid;
13
14/// Authorization context extracted from a request
15#[derive(Debug, Clone)]
16pub enum AuthContext {
17    /// Authenticated user
18    User {
19        user_id: Uuid,
20        tenant_id: Uuid,
21        roles: Vec<String>,
22    },
23
24    /// Owner of a specific resource
25    Owner {
26        user_id: Uuid,
27        tenant_id: Uuid,
28        resource_id: Uuid,
29        resource_type: String,
30    },
31
32    /// Service-to-service communication
33    Service {
34        service_name: String,
35        tenant_id: Option<Uuid>,
36    },
37
38    /// System administrator
39    Admin { admin_id: Uuid },
40
41    /// No authentication (public access)
42    Anonymous,
43}
44
45impl AuthContext {
46    /// Get tenant_id from context if available
47    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    /// Check if context represents an admin
58    pub fn is_admin(&self) -> bool {
59        matches!(self, AuthContext::Admin { .. })
60    }
61
62    /// Check if context represents a service
63    pub fn is_service(&self) -> bool {
64        matches!(self, AuthContext::Service { .. })
65    }
66
67    /// Get user_id if available
68    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/// Authorization policy for an operation
78#[derive(Debug, Clone)]
79pub enum AuthPolicy {
80    /// Public access (no auth required)
81    Public,
82
83    /// Any authenticated user
84    Authenticated,
85
86    /// Owner of the resource only
87    Owner,
88
89    /// User must have one of these roles
90    HasRole(Vec<String>),
91
92    /// Service-to-service only
93    ServiceOnly,
94
95    /// Admin only
96    AdminOnly,
97
98    /// Combination of policies (AND)
99    And(Vec<AuthPolicy>),
100
101    /// Combination of policies (OR)
102    Or(Vec<AuthPolicy>),
103
104    /// Custom policy function
105    Custom(fn(&AuthContext) -> bool),
106}
107
108impl AuthPolicy {
109    /// Check if auth context satisfies this policy
110    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    /// Parse policy from string (for YAML config)
136    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, // Default
152        }
153    }
154}
155
156/// Trait for auth providers
157#[async_trait]
158pub trait AuthProvider: Send + Sync {
159    /// Extract auth context from HTTP request
160    async fn extract_context<B>(&self, req: &Request<B>) -> Result<AuthContext>;
161
162    /// Check if user is owner of a resource
163    async fn is_owner(
164        &self,
165        user_id: &Uuid,
166        resource_id: &Uuid,
167        resource_type: &str,
168    ) -> Result<bool>;
169
170    /// Check if user has a role
171    async fn has_role(&self, user_id: &Uuid, role: &str) -> Result<bool>;
172}
173
174/// Default no-auth provider (for development)
175pub 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}