Skip to main content

ravenrs_app/security/
mod.rs

1//! Who may do what: roles, permissions and how they are decided.
2//!
3//! Everything here is enforced by [`SecuredObjectSpace`], under the screens, so a call with no
4//! screen around it gets the same answer. What the screens hide on top of that is only courtesy.
5
6use std::collections::BTreeMap;
7use std::rc::Rc;
8
9use ravenrs_criteria::{Criteria, Environment, Scope, Value};
10
11use crate::{EntityInfo, FieldKind, Object};
12
13mod audit;
14mod directory;
15mod password;
16mod records;
17mod space;
18
19pub use audit::{AuditMode, AuditSettings, AuditTrail, AuditedObjectSpace};
20pub use directory::{
21    AuthError, Authenticator, Credentials, MIN_PASSWORD_CHARS, PasswordError, SecurityContext, SpaceMaker,
22    UserDirectory,
23};
24pub use password::{hash_password, verify_password};
25pub use records::{
26    ADMINISTRATORS_ROLE, SECURITY_ACTION_PERMISSION, SECURITY_AUDIT_ENTRY, SECURITY_MEMBER_PERMISSION,
27    SECURITY_NAVIGATION_PERMISSION, SECURITY_OBJECT_PERMISSION, SECURITY_ROLE, SECURITY_TYPE_PERMISSION,
28    SECURITY_USER, SECURITY_USER_ROLE, audit_entity, security_entities,
29};
30pub use space::SecuredObjectSpace;
31
32/// What can be done to a record.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
34#[non_exhaustive]
35#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
36pub enum Operation {
37    /// See it.
38    Read,
39    /// Change it.
40    Write,
41    /// Make a new one. A new record is also written, so it takes [`Write`](Self::Write) too.
42    Create,
43    /// Take it away.
44    Delete,
45}
46
47/// What a permission says.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
50pub enum Access {
51    /// It can be done.
52    Allow,
53    /// It cannot.
54    Deny,
55}
56
57/// What a role says about what no permission mentions.
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
60pub enum PermissionPolicy {
61    /// Nothing can be done.
62    #[default]
63    DenyAll,
64    /// Everything can be seen, and nothing changed.
65    ReadOnlyAll,
66    /// Everything can be done.
67    AllowAll,
68}
69
70#[cfg_attr(feature = "live", ravenrs_live::expose)]
71impl PermissionPolicy {
72    fn allows(self, operation: Operation) -> bool {
73        match self {
74            Self::DenyAll => false,
75            Self::ReadOnlyAll => operation == Operation::Read,
76            Self::AllowAll => true,
77        }
78    }
79}
80
81/// How the answers of several roles of one person come together.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
84pub enum RoleMerge {
85    /// One role that allows it is enough. Each role is asked apart, so what one denies does not
86    /// take away what another allows.
87    #[default]
88    AnyRole,
89    /// Every role has to allow it.
90    AllRoles,
91}
92
93/// An expression a permission depends on. One that does not read, or that fails on a record, never
94/// allows and always denies, so a mistake in it cannot open anything.
95#[derive(Clone, Debug)]
96struct Condition {
97    criteria: Option<Criteria>,
98}
99
100#[cfg_attr(feature = "live", ravenrs_live::expose)]
101impl Condition {
102    fn new(source: &str) -> Self {
103        Self { criteria: Criteria::parse(source).ok() }
104    }
105
106    /// Whether it holds for this record, given what is being said.
107    fn holds(&self, record: &Object, access: Access, environment: &Environment) -> bool {
108        let denies = access == Access::Deny;
109        match &self.criteria {
110            Some(criteria) => match Scope::new(environment).row(record).evaluate(criteria) {
111                Ok(Value::Bool(holds)) => holds,
112                _ => denies,
113            },
114            None => denies,
115        }
116    }
117}
118
119#[derive(Clone, Debug)]
120struct TypePermission {
121    entity: String,
122    operation: Operation,
123    access: Access,
124}
125
126#[derive(Clone, Debug)]
127struct ObjectPermission {
128    entity: String,
129    condition: Condition,
130    operation: Operation,
131    access: Access,
132}
133
134#[derive(Clone, Debug)]
135struct MemberPermission {
136    entity: String,
137    fields: Vec<String>,
138    condition: Option<Condition>,
139    operation: Operation,
140    access: Access,
141}
142
143/// A set of permissions a person can have.
144#[derive(Clone, Debug, Default)]
145#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
146pub struct Role {
147    name: String,
148    policy: PermissionPolicy,
149    administrative: bool,
150    edit_model: bool,
151    types: Vec<TypePermission>,
152    objects: Vec<ObjectPermission>,
153    members: Vec<MemberPermission>,
154    navigation: BTreeMap<String, Access>,
155    actions: BTreeMap<String, Access>,
156}
157
158#[cfg_attr(feature = "live", ravenrs_live::expose)]
159impl Role {
160    /// A role with this name, which allows nothing yet.
161    pub fn new(name: impl Into<String>) -> Self {
162        Self { name: name.into(), ..Self::default() }
163    }
164
165    /// Its name.
166    pub fn name(&self) -> &str {
167        &self.name
168    }
169
170    /// What it says about what no permission mentions.
171    pub fn policy(mut self, policy: PermissionPolicy) -> Self {
172        self.policy = policy;
173        self
174    }
175
176    /// It can do everything, and no permission takes anything away from it.
177    pub fn administrative(mut self) -> Self {
178        self.administrative = true;
179        self.edit_model = true;
180        self
181    }
182
183    /// Whether it can do everything.
184    pub fn is_administrative(&self) -> bool {
185        self.administrative
186    }
187
188    /// It can change the application's model while it runs.
189    pub fn can_edit_model(mut self) -> Self {
190        self.edit_model = true;
191        self
192    }
193
194    /// Says this about these operations on every record of an entity.
195    pub fn type_permission(
196        mut self,
197        entity: impl Into<String>,
198        operations: impl IntoIterator<Item = Operation>,
199        access: Access,
200    ) -> Self {
201        let entity = entity.into();
202        self.types.extend(operations.into_iter().map(|operation| TypePermission {
203            entity: entity.clone(),
204            operation,
205            access,
206        }));
207        self
208    }
209
210    /// Says this about these operations on the records an expression holds for. `?CurrentUserId`
211    /// and `?CurrentUserName` are the person. It comes before what is said about the whole entity.
212    pub fn object_permission(
213        mut self,
214        entity: impl Into<String>,
215        criteria: &str,
216        operations: impl IntoIterator<Item = Operation>,
217        access: Access,
218    ) -> Self {
219        let entity = entity.into();
220        self.objects.extend(operations.into_iter().map(|operation| ObjectPermission {
221            entity: entity.clone(),
222            condition: Condition::new(criteria),
223            operation,
224            access,
225        }));
226        self
227    }
228
229    /// Says this about reading or writing these fields, on every record or, with an expression,
230    /// on those it holds for. It comes before everything else, the expression first.
231    pub fn member_permission(
232        mut self,
233        entity: impl Into<String>,
234        fields: impl IntoIterator<Item = impl Into<String>>,
235        criteria: Option<&str>,
236        operations: impl IntoIterator<Item = Operation>,
237        access: Access,
238    ) -> Self {
239        let entity = entity.into();
240        let fields: Vec<String> = fields.into_iter().map(Into::into).collect();
241        self.members.extend(operations.into_iter().map(|operation| MemberPermission {
242            entity: entity.clone(),
243            fields: fields.clone(),
244            condition: criteria.map(Condition::new),
245            operation,
246            access,
247        }));
248        self
249    }
250
251    /// Whether this navigation item or group shows, by its id. An item comes before its group. It
252    /// only hides: what protects records are the other permissions.
253    pub fn navigation_permission(mut self, item: impl Into<String>, access: Access) -> Self {
254        self.navigation.insert(item.into(), access);
255        self
256    }
257
258    /// Whether this action exists for it, by its id. A denied one goes away, whatever else says.
259    pub fn action_permission(mut self, action: impl Into<String>, access: Access) -> Self {
260        self.actions.insert(action.into(), access);
261        self
262    }
263
264    fn type_says(&self, operation: Operation, entity: &str) -> Option<Access> {
265        strongest(
266            self.types.iter().filter(|p| p.operation == operation && p.entity == entity).map(|p| p.access),
267        )
268    }
269
270    /// What it says about the records of this entity. With no record, an allowance that depends on
271    /// one counts, as something might fit it; a denial that depends on one does not.
272    fn objects_say(
273        &self,
274        operation: Operation,
275        entity: &str,
276        record: Option<&Object>,
277        environment: &Environment,
278    ) -> Option<Access> {
279        strongest(
280            self.objects
281                .iter()
282                .filter(|p| p.operation == operation && p.entity == entity)
283                .filter(|p| match record {
284                    Some(record) => p.condition.holds(record, p.access, environment),
285                    None => p.access == Access::Allow && p.condition.criteria.is_some(),
286                })
287                .map(|p| p.access),
288        )
289    }
290
291    fn members_say(
292        &self,
293        operation: Operation,
294        entity: &str,
295        field: &str,
296        record: Option<&Object>,
297        environment: &Environment,
298    ) -> Option<Access> {
299        let about = || {
300            self.members
301                .iter()
302                .filter(|p| p.operation == operation && p.entity == entity)
303                .filter(|p| p.fields.iter().any(|name| name == field))
304        };
305        let by_criteria = strongest(
306            about()
307                .filter_map(|p| p.condition.as_ref().map(|condition| (p, condition)))
308                .filter(|(p, condition)| match record {
309                    Some(record) => condition.holds(record, p.access, environment),
310                    None => p.access == Access::Allow && condition.criteria.is_some(),
311                })
312                .map(|(p, _)| p.access),
313        );
314        by_criteria.or_else(|| strongest(about().filter(|p| p.condition.is_none()).map(|p| p.access)))
315    }
316
317    /// What it says about the records of an aggregated collection: they belong to their owner, so
318    /// what it says about the owner, or about the collection, goes for them. Where that depends on
319    /// which owner, it cannot be told from here, so they stay closed unless said otherwise.
320    fn owner_says(&self, operation: Operation, entity: &str, entities: &[EntityInfo]) -> Option<Access> {
321        let asked = if operation == Operation::Read { Operation::Read } else { Operation::Write };
322        strongest(entities.iter().flat_map(|owner| {
323            owner
324                .fields
325                .iter()
326                .filter(move |field| {
327                    field.aggregated
328                        && matches!(&field.kind, FieldKind::Collection(target) if target == entity)
329                })
330                .filter_map(move |field| {
331                    let conditional =
332                        self.objects.iter().any(|p| p.entity == owner.name && p.operation == asked)
333                            || self.members.iter().any(|p| {
334                                p.operation == asked
335                                    && p.entity == owner.name
336                                    && p.condition.is_some()
337                                    && p.fields.contains(&field.name)
338                            });
339                    if conditional {
340                        return Some(Access::Deny);
341                    }
342                    strongest(
343                        self.members
344                            .iter()
345                            .filter(|p| {
346                                p.operation == asked
347                                    && p.entity == owner.name
348                                    && p.condition.is_none()
349                                    && p.fields.contains(&field.name)
350                            })
351                            .map(|p| p.access),
352                    )
353                    .or_else(|| self.type_says(asked, &owner.name))
354                })
355        }))
356    }
357
358    fn can(
359        &self,
360        operation: Operation,
361        entity: &str,
362        record: Option<&Object>,
363        entities: &[EntityInfo],
364        environment: &Environment,
365    ) -> bool {
366        if self.administrative {
367            return true;
368        }
369        let said = self
370            .objects_say(operation, entity, record, environment)
371            .or_else(|| self.type_says(operation, entity))
372            .or_else(|| self.owner_says(operation, entity, entities));
373        said.map_or_else(|| self.policy.allows(operation), |access| access == Access::Allow)
374    }
375
376    fn can_field(
377        &self,
378        operation: Operation,
379        entity: &str,
380        field: &str,
381        record: Option<&Object>,
382        entities: &[EntityInfo],
383        environment: &Environment,
384    ) -> bool {
385        if self.administrative {
386            return true;
387        }
388        match self.members_say(operation, entity, field, record, environment) {
389            Some(access) => access == Access::Allow,
390            None => self.can(operation, entity, record, entities, environment),
391        }
392    }
393
394    fn depends_on_record(&self, entity: &str) -> bool {
395        !self.administrative
396            && (self.objects.iter().any(|p| p.entity == entity)
397                || self.members.iter().any(|p| p.entity == entity && p.condition.is_some()))
398    }
399
400    /// Merges what a role in code says with what its record says. Only ever adds to the record.
401    fn merged_with(mut self, code: &Role) -> Self {
402        self.administrative |= code.administrative;
403        self.edit_model |= code.edit_model;
404        self.types.extend(code.types.iter().cloned());
405        self.objects.extend(code.objects.iter().cloned());
406        self.members.extend(code.members.iter().cloned());
407        for (item, access) in &code.navigation {
408            self.navigation.entry(item.clone()).or_insert(*access);
409        }
410        for (action, access) in &code.actions {
411            self.actions.entry(action.clone()).or_insert(*access);
412        }
413        if self.policy == PermissionPolicy::DenyAll {
414            self.policy = code.policy;
415        }
416        self
417    }
418}
419
420/// A denial beats an allowance said at the same level.
421fn strongest(said: impl Iterator<Item = Access>) -> Option<Access> {
422    let mut found = None;
423    for access in said {
424        if access == Access::Deny {
425            return Some(Access::Deny);
426        }
427        found = Some(access);
428    }
429    found
430}
431
432/// Who is using the application.
433#[derive(Clone, Debug)]
434#[non_exhaustive]
435#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
436pub struct Identity {
437    /// The key of their record, for `?CurrentUserId`.
438    pub key: Value,
439    /// Their name, for `?CurrentUserName` and the audit trail.
440    pub name: String,
441    /// Their roles.
442    pub roles: Vec<Role>,
443    /// They have to choose a new password before anything else.
444    pub must_change_password: bool,
445}
446
447#[cfg_attr(feature = "live", ravenrs_live::expose)]
448impl Identity {
449    /// Someone with this name and these roles, as another way of signing in would say.
450    pub fn new(key: impl Into<Value>, name: impl Into<String>, roles: Vec<Role>) -> Self {
451        Self { key: key.into(), name: name.into(), roles, must_change_password: false }
452    }
453}
454
455/// What one person can do: the answers of their roles, brought together.
456#[derive(Debug)]
457#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
458pub struct Permissions {
459    identity: Identity,
460    merge: RoleMerge,
461    entities: Rc<Vec<EntityInfo>>,
462    environment: Environment,
463}
464
465#[cfg_attr(feature = "live", ravenrs_live::expose)]
466impl Permissions {
467    /// What this person can do over these entities.
468    pub fn new(identity: Identity, entities: Vec<EntityInfo>, merge: RoleMerge) -> Self {
469        let environment = crate::environment()
470            .parameter("CurrentUserId", identity.key.clone())
471            .parameter("CurrentUserName", identity.name.clone());
472        Self { identity, merge, entities: Rc::new(entities), environment }
473    }
474
475    /// Who this is about.
476    pub fn identity(&self) -> &Identity {
477        &self.identity
478    }
479
480    /// What expressions about this person are evaluated with.
481    pub fn environment(&self) -> &Environment {
482        &self.environment
483    }
484
485    pub(crate) fn entity(&self, name: &str) -> Option<&EntityInfo> {
486        self.entities.iter().find(|entity| entity.name == name)
487    }
488
489    fn agreed(&self, ask: impl Fn(&Role) -> bool) -> bool {
490        let roles = &self.identity.roles;
491        match self.merge {
492            RoleMerge::AnyRole => roles.iter().any(ask),
493            RoleMerge::AllRoles => !roles.is_empty() && roles.iter().all(ask),
494        }
495    }
496
497    /// Whether they can do everything.
498    pub fn is_administrator(&self) -> bool {
499        self.identity.roles.iter().any(|role| role.administrative)
500    }
501
502    /// Whether they can change the application's model while it runs.
503    pub fn can_edit_model(&self) -> bool {
504        self.identity.roles.iter().any(|role| role.edit_model)
505    }
506
507    /// What holds whatever roles say: who manages people decides who can do what, so only an
508    /// administrator reaches those records, and the audit trail can only be read.
509    fn built_in(&self, operation: Operation, entity: &str) -> Option<bool> {
510        if entity == SECURITY_AUDIT_ENTRY {
511            return Some(operation == Operation::Read && self.is_administrator());
512        }
513        records::is_security_entity(entity).then(|| self.is_administrator())
514    }
515
516    /// Whether they can do this to this record or, with none, to some record of the entity.
517    pub fn can(&self, operation: Operation, entity: &str, record: Option<&Object>) -> bool {
518        if let Some(answer) = self.built_in(operation, entity) {
519            return answer;
520        }
521        self.agreed(|role| role.can(operation, entity, record, &self.entities, &self.environment))
522    }
523
524    /// Whether they can read or write this field of this record or, with none, of some record.
525    pub fn can_field(
526        &self,
527        operation: Operation,
528        entity: &str,
529        field: &str,
530        record: Option<&Object>,
531    ) -> bool {
532        self.can_field_within(operation, entity, field, record, 0)
533    }
534
535    fn can_field_within(
536        &self,
537        operation: Operation,
538        entity: &str,
539        field: &str,
540        record: Option<&Object>,
541        depth: usize,
542    ) -> bool {
543        if let Some(answer) = self.built_in(operation, entity)
544            && !answer
545        {
546            return false;
547        }
548        let info = self.entity(entity).and_then(|info| info.field_named(field));
549        if info.is_some_and(|info| info.secret) {
550            return false;
551        }
552        let allowed = self.agreed(|role| {
553            role.can_field(operation, entity, field, record, &self.entities, &self.environment)
554        });
555        if !allowed {
556            return false;
557        }
558        // A calculated value says what it is made of, so it takes being able to read all of that.
559        match info.and_then(|info| info.calculated.as_deref()) {
560            Some(expression) if operation == Operation::Read => {
561                depth < 8
562                    && Criteria::parse(expression).is_ok_and(|criteria| {
563                        criteria.properties().iter().all(|path| {
564                            let head = path.split('.').next().unwrap_or(path).trim_start_matches('^');
565                            head == field
566                                || self.can_field_within(Operation::Read, entity, head, record, depth + 1)
567                        })
568                    })
569            }
570            _ => true,
571        }
572    }
573
574    /// Whether what they can do with this entity changes from record to record.
575    pub(crate) fn depends_on_record(&self, entity: &str) -> bool {
576        self.identity.roles.iter().any(|role| role.depends_on_record(entity))
577    }
578
579    /// Whether this navigation item shows, inside this group, if it has one. An item comes before
580    /// its group, and what no role mentions shows.
581    pub fn can_navigate(&self, group: Option<&str>, item: &str) -> bool {
582        if self.identity.roles.iter().any(|role| role.administrative) {
583            return true;
584        }
585        self.agreed(|role| {
586            let said =
587                role.navigation.get(item).or_else(|| group.and_then(|group| role.navigation.get(group)));
588            said != Some(&Access::Deny)
589        })
590    }
591
592    /// Whether this action exists for them. Any role that denies it takes it away.
593    pub fn can_use_action(&self, action: &str) -> bool {
594        if self.is_administrator() {
595            return true;
596        }
597        !self.identity.roles.iter().any(|role| role.actions.get(action) == Some(&Access::Deny))
598    }
599}