Skip to main content

ravenrs_app/security/
space.rs

1//! The permissions, applied to every read and every write.
2
3use std::rc::Rc;
4
5use ravenrs_criteria::{Criteria, Value};
6
7use super::{Operation, SecurityContext};
8use crate::space::select;
9use crate::{
10    Change, EntityInfo, FieldInfo, Object, ObjectSpace, Query, SpaceError, SpaceEvent, SpaceWatcher,
11};
12
13/// A unit of work that only does what the person signed in can do, whoever asks: a screen, an
14/// action or a call with no screen around it.
15///
16/// A record they cannot read is not there. A field they cannot read reads as empty, also for
17/// filters and sorting, so a filter cannot be used to guess it, and writing it back empty leaves
18/// what is stored.
19#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
20pub struct SecuredObjectSpace {
21    inner: Box<dyn ObjectSpace>,
22    context: Rc<SecurityContext>,
23}
24
25impl std::fmt::Debug for SecuredObjectSpace {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("SecuredObjectSpace").field("context", &self.context).finish_non_exhaustive()
28    }
29}
30
31/// Fields that hold a value, as opposed to listing other records.
32fn stored_fields(entity: &EntityInfo) -> impl Iterator<Item = &FieldInfo> {
33    entity.fields.iter().filter(|field| !field.kind.is_collection())
34}
35
36/// The field a path of an expression starts from.
37fn head(path: &str) -> &str {
38    path.split('.').next().unwrap_or(path).trim_start_matches('^')
39}
40
41/// Whether an expression reaches beyond the fields of the record, as a database could and memory
42/// cannot: into a reference, or a collection.
43fn reaches_out(criteria: &Criteria) -> bool {
44    let mut found = false;
45    criteria.walk(&mut |part| match part {
46        Criteria::Property(path) if path.len() > 1 => found = true,
47        Criteria::Aggregate(_) => found = true,
48        _ => {}
49    });
50    found
51}
52
53#[cfg_attr(feature = "live", ravenrs_live::expose)]
54impl SecuredObjectSpace {
55    /// Over this, for the person of this context.
56    pub fn new(inner: Box<dyn ObjectSpace>, context: Rc<SecurityContext>) -> Self {
57        Self { inner, context }
58    }
59
60    fn entity(&self, name: &str) -> Result<EntityInfo, SpaceError> {
61        self.context
62            .with(|permissions| permissions.entity(name).cloned())
63            .ok_or_else(|| SpaceError::UnknownEntity(name.to_owned()))
64    }
65
66    /// The record as they can see it; nothing if they cannot see it at all. A field of theirs
67    /// keeps its value only if it can be read on this very record.
68    fn visible(&self, entity: &EntityInfo, mut record: Object) -> Option<Object> {
69        self.context.with(|permissions| {
70            let hidden: Vec<&str> = stored_fields(entity)
71                .filter(|field| !field.is_key)
72                .filter(|field| {
73                    !permissions.can_field(Operation::Read, &entity.name, &field.name, Some(&record))
74                })
75                .map(|field| field.name.as_str())
76                .collect();
77            let whole = permissions.can(Operation::Read, &entity.name, Some(&record));
78            // A permission on a field can open a record its entity keeps closed, but only that.
79            let some_field = stored_fields(entity).filter(|field| !field.is_key).count() > hidden.len();
80            if !whole && !some_field {
81                return None;
82            }
83            for name in hidden {
84                if record.values().contains_key(name) {
85                    record.set(name, Value::Null);
86                }
87            }
88            Some(record)
89        })
90    }
91
92    /// Whether a query can go to storage as it is, because no record and no field it mentions
93    /// depends on anything storage does not know.
94    fn storage_can_answer(&self, entity: &EntityInfo, query: &Query) -> bool {
95        self.context.with(|permissions| {
96            if permissions.depends_on_record(&entity.name)
97                || !permissions.can(Operation::Read, &entity.name, None)
98            {
99                return false;
100            }
101            let readable = |name: &str| {
102                entity.field_named(name).is_some_and(|field| !field.secret)
103                    && permissions.can_field(Operation::Read, &entity.name, name, None)
104            };
105            let filter_fits = query.filter.as_ref().is_none_or(|filter| {
106                !reaches_out(filter) && filter.properties().iter().all(|path| readable(head(path)))
107            });
108            filter_fits && query.sort.iter().all(|(field, _)| readable(field))
109        })
110    }
111
112    fn check_new(&self, entity: &EntityInfo, object: &Object) -> Result<(), SpaceError> {
113        let fresh = entity.new_object();
114        self.context.with(|permissions| {
115            let name = &entity.name;
116            let mut object = object.clone();
117            entity.compute(&mut object, permissions.environment());
118            let object = &object;
119            if !permissions.can(Operation::Create, name, Some(object))
120                || !permissions.can(Operation::Write, name, Some(object))
121            {
122                return Err(self.context.denied());
123            }
124            for field in stored_fields(entity).filter(|field| !field.is_key && field.calculated.is_none()) {
125                let value = object.get(&field.name);
126                if value != fresh.get(&field.name)
127                    && !permissions.can_field(Operation::Write, name, &field.name, Some(object))
128                {
129                    return Err(self.context.denied());
130                }
131            }
132            Ok(())
133        })
134    }
135
136    /// What is written over what is stored: every field changed has to be theirs to write, before
137    /// and after, and a field they cannot read written back empty stays as it was. Nothing changed
138    /// means nothing to write, as writing it would bring back the whole record as it stood.
139    fn over_stored(
140        &self,
141        entity: &EntityInfo,
142        mut object: Object,
143        stored: &Object,
144    ) -> Result<Option<Object>, SpaceError> {
145        self.context.with(|permissions| {
146            let name = &entity.name;
147            let mut changed = false;
148            let writable = || stored_fields(entity).filter(|field| field.calculated.is_none());
149            for field in writable() {
150                let (new, old) = (object.get(&field.name), stored.get(&field.name));
151                if new == old {
152                    continue;
153                }
154                if new == Value::Null
155                    && !permissions.can_field(Operation::Read, name, &field.name, Some(stored))
156                {
157                    object.set(&field.name, old);
158                    continue;
159                }
160                if !permissions.can_field(Operation::Write, name, &field.name, Some(stored)) {
161                    return Err(self.context.denied());
162                }
163                changed = true;
164            }
165            if !changed {
166                return Ok(None);
167            }
168            // What the rules read after the change is worked out here, and not taken from the caller.
169            let mut after = object.clone();
170            entity.compute(&mut after, permissions.environment());
171            for field in writable() {
172                if after.get(&field.name) != stored.get(&field.name)
173                    && !permissions.can_field(Operation::Write, name, &field.name, Some(&after))
174                {
175                    return Err(self.context.denied());
176                }
177            }
178            Ok(Some(object))
179        })
180    }
181}
182
183/// A waiting change as the person can see it: what they cannot read goes empty, as a save fills it
184/// in from storage for the commit.
185fn as_seen(context: &SecurityContext, change: &Change) -> Change {
186    let masked = |object: &Object| {
187        context.with(|permissions| {
188            let Some(entity) = permissions.entity(object.entity()) else { return object.clone() };
189            let mut seen = object.clone();
190            for field in stored_fields(entity).filter(|field| !field.is_key) {
191                if !permissions.can_field(Operation::Read, &entity.name, &field.name, Some(object)) {
192                    seen.set(&field.name, Value::Null);
193                }
194            }
195            seen
196        })
197    };
198    match change {
199        Change::New(object) => Change::New(masked(object)),
200        Change::Updated(object) => Change::Updated(masked(object)),
201        Change::Deleted { entity, key } => Change::Deleted { entity: entity.clone(), key: key.clone() },
202    }
203}
204
205#[cfg_attr(feature = "live", ravenrs_live::expose(part = "objectspace"))]
206impl ObjectSpace for SecuredObjectSpace {
207    fn entities(&self) -> Vec<EntityInfo> {
208        self.inner.entities()
209    }
210
211    fn query(&self, entity: &str, query: &Query) -> Result<Vec<Object>, SpaceError> {
212        let info = self.entity(entity)?;
213        if self.storage_can_answer(&info, query) {
214            let found = self.inner.query(entity, query)?;
215            return Ok(found.into_iter().filter_map(|record| self.visible(&info, record)).collect());
216        }
217        crate::space::check_query(&info, query)?;
218        // Everything is read, and only what they can see gets filtered, sorted and paged, so the
219        // size of a page and the order say nothing about the rest. Everything whole, since a
220        // permission can depend on a field the list does not show.
221        let everything = self.inner.query(entity, &Query::all())?;
222        let seen: Vec<Object> =
223            everything.into_iter().filter_map(|record| self.visible(&info, record)).collect();
224        self.context.with(|permissions| select(&info, seen, query, permissions.environment()))
225    }
226
227    fn find(&self, entity: &str, key: &Value) -> Result<Option<Object>, SpaceError> {
228        let info = self.entity(entity)?;
229        Ok(self.inner.find(entity, key)?.and_then(|record| self.visible(&info, record)))
230    }
231
232    fn new_object(&mut self, entity: &str) -> Result<Object, SpaceError> {
233        let allowed = self.context.with(|permissions| permissions.can(Operation::Create, entity, None));
234        if !allowed {
235            return Err(self.context.denied());
236        }
237        self.inner.new_object(entity)
238    }
239
240    fn save(&mut self, object: Object) -> Result<(), SpaceError> {
241        let info = self.entity(object.entity())?;
242        let key = info.key_field().map(|field| object.get(&field.name)).unwrap_or(Value::Null);
243        match self.inner.find(&info.name, &key)? {
244            Some(stored) => match self.over_stored(&info, object, &stored)? {
245                Some(written) => self.inner.save(written),
246                None => Ok(()),
247            },
248            None => {
249                self.check_new(&info, &object)?;
250                self.inner.save(object)
251            }
252        }
253    }
254
255    fn insert(&mut self, object: Object) -> Result<(), SpaceError> {
256        let info = self.entity(object.entity())?;
257        self.check_new(&info, &object)?;
258        self.inner.insert(object)
259    }
260
261    fn delete(&mut self, entity: &str, key: &Value) -> Result<(), SpaceError> {
262        let Some(stored) = self.inner.find(entity, key)? else { return self.inner.delete(entity, key) };
263        let allowed =
264            self.context.with(|permissions| permissions.can(Operation::Delete, entity, Some(&stored)));
265        if !allowed {
266            return Err(self.context.denied());
267        }
268        self.inner.delete(entity, key)
269    }
270
271    fn changes(&self) -> Vec<Change> {
272        let context = &self.context;
273        self.inner.changes().iter().map(|change| as_seen(context, change)).collect()
274    }
275
276    fn is_modified(&self) -> bool {
277        self.inner.is_modified()
278    }
279
280    fn commit(&mut self) -> Result<(), SpaceError> {
281        let touched = self.inner.changes().iter().any(|change| match change {
282            Change::New(object) | Change::Updated(object) => self.context.touches_security(object.entity()),
283            Change::Deleted { entity, .. } => self.context.touches_security(entity),
284        });
285        // What is waiting reads as if it were kept, so this sees the result of the commit.
286        if touched && !self.context.directory().has_active_administrator(&*self.inner)? {
287            return Err(SpaceError::Refused(
288                "At least one active user has to keep an administrative role.".to_owned(),
289            ));
290        }
291        self.inner.commit()?;
292        if touched {
293            self.context.reload()?;
294        }
295        Ok(())
296    }
297
298    fn rollback(&mut self) {
299        self.inner.rollback();
300    }
301
302    fn refresh(&mut self) {
303        self.inner.refresh();
304    }
305
306    fn on_event(&mut self, mut watcher: SpaceWatcher) {
307        // Whoever watches sees what the person can see, and not what is going to storage.
308        let context = self.context.clone();
309        self.inner.on_event(Box::new(move |event| match event {
310            SpaceEvent::Committing(changes) => {
311                let seen: Vec<Change> = changes.iter().map(|change| as_seen(&context, change)).collect();
312                watcher(SpaceEvent::Committing(&seen))
313            }
314            SpaceEvent::Committed(changes) => {
315                let seen: Vec<Change> = changes.iter().map(|change| as_seen(&context, change)).collect();
316                watcher(SpaceEvent::Committed(&seen))
317            }
318            other => watcher(other),
319        }));
320    }
321}