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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//! Synchronous, decision-returning enforcement immediately before a mutation writes.
//!
//! This is the engine half of the `before:mutation` trigger. Where
//! [`Authorizer`](crate::security::Authorizer) answers "may this principal run
//! this operation?", a [`BeforeMutationGate`] answers the broader question a
//! business rule asks: "given the arguments this write is about to run with, may
//! it run at all, and are these the arguments it should run with?" The engine
//! *enforces* the answer; the *decision* is delegated to an app-supplied trait
//! object — in `fraiseql-server` that object runs the compiled schema's
//! `before:mutation` function chain.
//!
//! # Why it lives at the chokepoint
//!
//! `before:mutation` is enforcement, so it is worth nothing unless it is
//! unbypassable. It used to run once per HTTP request in the GraphQL handler,
//! against the request's `variables`, keyed on `parse_query(…).root_field` — and
//! that shape left three ways to execute a mutation without running its chain
//! (#1327):
//!
//! 1. a **second root field**: the handler keyed on the *first* root, and since #759 the executor
//! runs every root serially, so the second one executed having run only the first one's chain;
//! 2. **inline arguments**: the chain was handed `request.variables`, so `guarded(input: { … })`
//! was invisible to it and a rewrite could only reach variables;
//! 3. the **REST write route**, which dispatched `after:mutation` only.
//!
//! The gate is therefore enforced from `execute_mutation_impl` — the single
//! point every mutation entry path converges on, the same place `requires_role`,
//! `requires_actor` (#966) and the `Authorizer` (#422) are enforced, and for the
//! same reason. Per root, in document order, immediately before that root's
//! write, with the arguments the write will actually run with.
//!
//! # Semantics
//!
//! - **Fail-closed**: any `Err` from [`BeforeMutationGate::before_mutation`] refuses the write. It
//! never falls through to "proceed with the original input", which would run a mutation the chain
//! declined to approve.
//! - **Keyed on the field name, never the alias**: two roots calling the same mutation differ only
//! by response key, so keying on the alias would run one chain twice and skip the other.
//! [`BeforeMutationRequest::mutation`] is the name; `response_key` is carried for diagnostics
//! only.
//! - **Resolved arguments**: [`BeforeMutationRequest::arguments`] is the merged view — request
//! variables plus inline literals, nested `$var` references already substituted — so a rule can
//! read what the write will use regardless of how the client spelled it.
//! - **Rewrites reach the write**: [`BeforeMutationOutcome::ProceedWith`] replaces the arguments
//! the engine binds to the SQL function, not just the request variables.
//!
//! # Wiring
//!
//! Register an implementation on [`RuntimeConfig`](crate::runtime::RuntimeConfig)
//! via
//! [`with_before_mutation_gate`](crate::runtime::RuntimeConfig::with_before_mutation_gate),
//! parallel to [`with_authorizer`](crate::runtime::RuntimeConfig::with_authorizer).
use Arc;
use crate::;
/// The write a [`BeforeMutationGate`] is being asked to adjudicate.
/// What the engine does next with the write.
/// A pluggable, decision-returning enforcement point that runs immediately before
/// a mutation writes.
///
/// Implementations must be `Send + Sync` to be shared across the async execution
/// path. Implementations run on the hot path of every matching write, so a gate
/// with nothing to say for a given mutation should return
/// [`BeforeMutationOutcome::Proceed`] as cheaply as it can.
///
/// # Example
///
/// ```
/// use fraiseql_core::error::Result;
/// use fraiseql_core::security::{
/// BeforeMutationGate, BeforeMutationOutcome, BeforeMutationRequest,
/// };
///
/// /// Refuse any write whose `input.amount` exceeds a ceiling.
/// struct CapAmount {
/// ceiling: i64,
/// }
///
/// #[async_trait::async_trait]
/// impl BeforeMutationGate for CapAmount {
/// async fn before_mutation(
/// &self,
/// req: &BeforeMutationRequest<'_>,
/// ) -> Result<BeforeMutationOutcome> {
/// let amount = req.arguments.pointer("/input/amount").and_then(|v| v.as_i64());
/// match amount {
/// Some(value) if value > self.ceiling => Ok(BeforeMutationOutcome::Abort {
/// reason: format!("amount {value} exceeds the {} ceiling", self.ceiling),
/// }),
/// _ => Ok(BeforeMutationOutcome::Proceed),
/// }
/// }
/// }
/// ```
/// The engine-side enforcement of a configured [`BeforeMutationGate`].
///
/// Returns the replacement argument view when the gate rewrote it, `None` when the
/// arguments stand (including the zero-overhead case of no gate configured).
///
/// Called from `execute_mutation_impl`, after every static gate and after the
/// inline-argument merge, so `arguments` is what the write will bind from and a
/// refusal happens before anything is written.
///
/// # Errors
///
/// Returns [`FraiseQLError::Validation`] carrying the rule's own message when the
/// gate aborts, or the gate's own error unchanged when it could not decide.
pub async