Skip to main content

backbone_core/
write_guard.rs

1//! The write guard: the rules a write must satisfy, asked on every path the
2//! generic service writes through.
3//!
4//! [`GenericCrudService`](crate::service::GenericCrudService) consults its
5//! guard on create, update (full and partial, single and bulk), soft delete,
6//! restore and hard delete, after the write's resulting row is known and
7//! before anything is persisted. A guard answers with violations to refuse and
8//! violations to only report: a rule that runs in shadow is logged and the
9//! write proceeds, so a newly compiled rule shows what it would refuse before
10//! it refuses anything. The schema generator compiles a module's declared rules
11//! into guards; without one, every write is allowed ([`AllowAll`]).
12
13use async_trait::async_trait;
14
15use crate::violation::Violation;
16
17/// What kind of write is being checked.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WriteKind {
20    Create,
21    /// A full or partial update.
22    Update,
23    /// A soft delete.
24    Delete,
25    Restore,
26    /// Removal of a soft-deleted row for good.
27    HardDelete,
28}
29
30/// One write, as the guard sees it.
31#[derive(Debug)]
32#[non_exhaustive]
33pub struct WriteCtx<'a, E> {
34    pub kind: WriteKind,
35    /// The row as stored before the write; `None` on create.
36    pub before: Option<&'a E>,
37    /// The row the write would store; `None` on delete and hard delete.
38    pub after: Option<&'a E>,
39    /// Who is writing: the request's audit attribution, when the composing
40    /// service bound one; `None` for a job, a seed or an unscoped caller.
41    pub actor: Option<String>,
42    /// When the write happens, the `now()` a rule compares against.
43    pub at: chrono::DateTime<chrono::Utc>,
44}
45
46impl<'a, E> WriteCtx<'a, E> {
47    /// The context of one write, attributed to the ambient request's actor
48    /// and timed now.
49    pub fn new(kind: WriteKind, before: Option<&'a E>, after: Option<&'a E>) -> Self {
50        Self {
51            kind,
52            before,
53            after,
54            actor: backbone_orm::audit_context::current_request_audit()
55                .map(|a| a.actor)
56                .filter(|a| !a.is_empty()),
57            at: chrono::Utc::now(),
58        }
59    }
60}
61
62/// A guard's answer.
63#[derive(Debug, Default, Clone, PartialEq)]
64pub struct GuardOutcome {
65    /// Broken rules that refuse the write.
66    pub refuse: Vec<Violation>,
67    /// Broken rules running in shadow: reported, not refused.
68    pub shadow: Vec<Violation>,
69}
70
71impl GuardOutcome {
72    pub fn allow() -> Self {
73        Self::default()
74    }
75
76    /// Fold another guard's answer into this one.
77    pub fn merge(mut self, other: GuardOutcome) -> Self {
78        self.refuse.extend(other.refuse);
79        self.shadow.extend(other.shadow);
80        self
81    }
82}
83
84/// The rules one entity's writes must satisfy.
85#[async_trait]
86pub trait WriteGuard<E>: Send + Sync {
87    async fn check(&self, ctx: &WriteCtx<'_, E>) -> GuardOutcome;
88}
89
90/// A guard that refuses nothing: the default for an entity whose schema
91/// declares no rules.
92#[derive(Debug, Default, Clone, Copy)]
93pub struct AllowAll;
94
95#[async_trait]
96impl<E: Sync> WriteGuard<E> for AllowAll {
97    async fn check(&self, _ctx: &WriteCtx<'_, E>) -> GuardOutcome {
98        GuardOutcome::allow()
99    }
100}