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