1use crate::cache::MembershipCache;
4use crate::matrix::RoleMatrix;
5use crate::membership::MembershipStore;
6use crate::owner::OwnerRegistry;
7use crate::types::{
8 ability_error, is_privileged_role, Ability, Capabilities, Membership, ResourceRef, ScopeRef,
9 ROLE_OWNER,
10};
11use sova_core::{Error, Result};
12use sova_db::DbHandle;
13use std::collections::HashMap;
14use std::sync::Arc;
15
16pub struct AuthorizeEngine {
17 matrices: HashMap<&'static str, RoleMatrix>,
18 owners: OwnerRegistry,
19 membership_cache: MembershipCache,
20}
21
22impl AuthorizeEngine {
23 pub fn new(
24 matrices: HashMap<&'static str, RoleMatrix>,
25 owners: OwnerRegistry,
26 ) -> Self {
27 Self {
28 matrices,
29 owners,
30 membership_cache: MembershipCache::new(),
31 }
32 }
33
34 pub fn membership_cache(&self) -> &MembershipCache {
35 &self.membership_cache
36 }
37
38 pub fn matrix(&self, scope_kind: &str) -> Option<&RoleMatrix> {
39 self.matrices.get(scope_kind)
40 }
41
42 pub async fn membership(
43 &self,
44 db: &DbHandle,
45 scope: ScopeRef,
46 user_id: i64,
47 ) -> Result<Option<Membership>> {
48 if let Some(hit) = self.membership_cache.get(&scope, user_id) {
49 return Ok(Some(hit));
50 }
51 let found = MembershipStore::find(db, scope.clone(), user_id).await?;
52 if let Some(ref m) = found {
53 self.membership_cache.put(m.clone());
54 }
55 Ok(found)
56 }
57
58 pub async fn require_member(
59 &self,
60 db: &DbHandle,
61 scope: ScopeRef,
62 user_id: i64,
63 ) -> Result<Membership> {
64 self.membership(db, scope, user_id)
65 .await?
66 .ok_or(Error::NotFound)
67 }
68
69 pub async fn require_privileged(
70 &self,
71 db: &DbHandle,
72 scope: ScopeRef,
73 user_id: i64,
74 ) -> Result<Membership> {
75 let m = self.require_member(db, scope, user_id).await?;
76 if !is_privileged_role(&m.role) {
77 return Err(Error::custom(403, "scope admin required"));
78 }
79 Ok(m)
80 }
81
82 pub async fn require_owner_role(
83 &self,
84 db: &DbHandle,
85 scope: ScopeRef,
86 user_id: i64,
87 ) -> Result<Membership> {
88 let m = self.require_member(db, scope, user_id).await?;
89 if m.role != ROLE_OWNER {
90 return Err(Error::custom(403, "scope owner required"));
91 }
92 Ok(m)
93 }
94
95 pub async fn is_privileged(
96 &self,
97 db: &DbHandle,
98 scope: ScopeRef,
99 user_id: i64,
100 ) -> Result<bool> {
101 Ok(self
102 .membership(db, scope, user_id)
103 .await?
104 .is_some_and(|m| is_privileged_role(&m.role)))
105 }
106
107 pub async fn effective(
108 &self,
109 db: &DbHandle,
110 scope: ScopeRef,
111 user_id: i64,
112 resource: Option<ResourceRef>,
113 ) -> Result<Capabilities> {
114 let m = self.require_member(db, scope.clone(), user_id).await?;
115 let matrix = self
116 .matrices
117 .get(scope.kind.as_str())
118 .ok_or_else(|| Error::Internal(format!("no matrix for scope kind {}", scope.kind)))?;
119 let is_owner = self.owners.is_owner(db, resource, user_id).await?;
120 Ok(matrix.capabilities(&m.role, resource, is_owner))
121 }
122
123 pub async fn authorize(
124 &self,
125 db: &DbHandle,
126 scope: ScopeRef,
127 user_id: i64,
128 ability: Ability,
129 resource: Option<ResourceRef>,
130 ) -> Result<Capabilities> {
131 let caps = self.effective(db, scope, user_id, resource).await?;
132 if !caps.has(ability) {
133 return Err(ability_error(ability));
134 }
135 Ok(caps)
136 }
137
138 pub async fn can(
139 &self,
140 db: &DbHandle,
141 scope: ScopeRef,
142 user_id: i64,
143 ability: Ability,
144 resource: Option<ResourceRef>,
145 ) -> Result<bool> {
146 Ok(self
147 .effective(db, scope, user_id, resource)
148 .await
149 .map(|c| c.has(ability))
150 .unwrap_or(false))
151 }
152
153 pub fn owners(&self) -> &OwnerRegistry {
154 &self.owners
155 }
156}
157
158pub type SharedEngine = Arc<AuthorizeEngine>;