Skip to main content

sova_scope/
engine.rs

1//! Authorization engine.
2
3use 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>;