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}