Skip to main content

sift_query_core/query/scope/
mod.rs

1//! Service-owned row and group authorization scopes.
2use crate::policy::budget::logical_values;
3use crate::{
4    Condition, FieldId, FieldRef, ParamError, PredicateArena, PredicateNode, SearchEntity,
5};
6mod group;
7mod policy;
8mod row;
9pub use group::GroupScopeBuilder;
10pub use policy::ScopeProvider;
11pub use row::ServerScopeBuilder;
12
13use std::marker::PhantomData;
14
15/// Server-owned field visibility and queryability policy.
16pub struct FieldAccessPolicy<E: SearchEntity> {
17    schema_fingerprint: u64,
18    allowed: Vec<bool>,
19    _entity: PhantomData<fn() -> E>,
20}
21
22impl<E: SearchEntity> Clone for FieldAccessPolicy<E> {
23    fn clone(&self) -> Self {
24        Self {
25            schema_fingerprint: self.schema_fingerprint,
26            allowed: self.allowed.clone(),
27            _entity: PhantomData,
28        }
29    }
30}
31
32impl<E: SearchEntity> std::fmt::Debug for FieldAccessPolicy<E> {
33    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        formatter
35            .debug_struct("FieldAccessPolicy")
36            .field("schema_fingerprint", &self.schema_fingerprint)
37            .field("allowed", &self.allowed)
38            .finish()
39    }
40}
41
42impl<E: SearchEntity> PartialEq for FieldAccessPolicy<E> {
43    fn eq(&self, other: &Self) -> bool {
44        self.schema_fingerprint == other.schema_fingerprint && self.allowed == other.allowed
45    }
46}
47
48impl<E: SearchEntity> Eq for FieldAccessPolicy<E> {}
49
50impl<E: SearchEntity> FieldAccessPolicy<E> {
51    #[must_use]
52    pub fn unrestricted() -> Self {
53        Self {
54            schema_fingerprint: E::META.schema_fingerprint,
55            allowed: vec![true; E::META.fields.len()],
56            _entity: PhantomData,
57        }
58    }
59
60    #[must_use]
61    pub fn none() -> Self {
62        Self {
63            schema_fingerprint: E::META.schema_fingerprint,
64            allowed: vec![false; E::META.fields.len()],
65            _entity: PhantomData,
66        }
67    }
68
69    #[must_use]
70    pub fn builder() -> FieldAccessPolicyBuilder<E> {
71        FieldAccessPolicyBuilder {
72            policy: Self::none(),
73            error: None,
74        }
75    }
76
77    #[must_use]
78    pub fn allows(&self, field: FieldId) -> bool {
79        self.allowed
80            .get(usize::from(field.0))
81            .copied()
82            .unwrap_or(false)
83    }
84
85    #[must_use]
86    pub fn schema_fingerprint(&self) -> u64 {
87        self.schema_fingerprint
88    }
89
90    pub(crate) fn intersect(&self, other: &Self) -> Self {
91        let allowed = self
92            .allowed
93            .iter()
94            .zip(&other.allowed)
95            .map(|(left, right)| *left && *right)
96            .collect();
97        Self {
98            schema_fingerprint: self.schema_fingerprint,
99            allowed,
100            _entity: PhantomData,
101        }
102    }
103}
104
105/// Builder for an explicit least-privilege field policy.
106#[derive(Debug)]
107pub struct FieldAccessPolicyBuilder<E: SearchEntity> {
108    policy: FieldAccessPolicy<E>,
109    error: Option<ParamError>,
110}
111
112impl<E: SearchEntity> FieldAccessPolicyBuilder<E> {
113    #[must_use]
114    pub fn allow<T, K>(self, field: FieldRef<E, T, K>) -> Self {
115        self.allow_id(field.id())
116    }
117
118    #[must_use]
119    pub fn allow_id(mut self, field: FieldId) -> Self {
120        if self.error.is_some() {
121            return self;
122        }
123        let Some(slot) = self.policy.allowed.get_mut(usize::from(field.0)) else {
124            self.error = Some(ParamError::UnknownField(format!("#{}", field.0)));
125            return self;
126        };
127        *slot = true;
128        self
129    }
130
131    #[must_use]
132    pub fn allow_all(mut self) -> Self {
133        if self.error.is_none() {
134            self.policy.allowed.fill(true);
135        }
136        self
137    }
138
139    pub fn build(self) -> Result<FieldAccessPolicy<E>, ParamError> {
140        self.error.map_or(Ok(self.policy), Err)
141    }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct ScopeLimits {
146    pub max_conditions: usize,
147    pub max_logical_values: usize,
148}
149
150impl Default for ScopeLimits {
151    fn default() -> Self {
152        Self {
153            max_conditions: 64,
154            max_logical_values: 512,
155        }
156    }
157}
158
159fn validate_predicates<'a>(
160    predicates: impl Iterator<Item = &'a PredicateArena> + Clone,
161    limits: ScopeLimits,
162) -> Result<(), ParamError> {
163    let conditions = predicates
164        .flat_map(PredicateArena::nodes)
165        .filter_map(|node| {
166            if let PredicateNode::Condition(condition) = node {
167                Some(condition)
168            } else {
169                None
170            }
171        });
172    validate_conditions(conditions, limits)
173}
174
175fn validate_conditions<'a>(
176    conditions: impl Iterator<Item = &'a Condition> + Clone,
177    limits: ScopeLimits,
178) -> Result<(), ParamError> {
179    let condition_count = conditions.clone().count();
180    if condition_count > limits.max_conditions {
181        return Err(ParamError::TooComplex("server scope condition count"));
182    }
183    logical_values(
184        conditions.flat_map(|condition| &condition.values),
185        limits.max_logical_values,
186        "server scope logical value count",
187    )?;
188    Ok(())
189}