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
241
242
243
244
245
246
247
248
249
250
251
252
253
//! AuthzPolicy capability trait. See `plan/ecosystem/02-capabilities.md §AuthzPolicy`.
//!
//! An `AuthzPolicyPlugin` answers *"given this request context, is the
//! action allowed?"* using an externalised policy language — Cedar, CEL,
//! OPA/Rego, or a custom engine. It sits one layer **above**
//! [`crate::auth::AuthPlugin`]: auth establishes *who* the caller is,
//! authz-policy decides *what* they may do with that identity.
//!
//! # Why the return shape is an enum, not `Result<_, String>`
//!
//! This is the one capability in `bext-plugin-api` where a flat enum is
//! load-bearing. Callers branch on three genuinely distinct outcomes:
//!
//! 1. **Allow.** Let the request proceed untouched.
//! 2. **Deny.** Short-circuit with a 403, forwarding the reason to the
//! logs and (optionally) to the client. The reason string is policy
//! author–controlled ("principal lacks `posts:write` on `posts/123`")
//! and the host must not discard it.
//! 3. **Mutate.** Let the request proceed, but **rewrite the response**
//! on the way out — inject headers (CSP, `X-Tenant`, audit trailers)
//! or substitute the body (redact fields, stamp a watermark). Cedar's
//! response-side obligations and a handful of custom engines need
//! this; without it the trait cannot represent what they do.
//!
//! Folding these into `Result<_, String>` would collapse Mutate into
//! Allow, which silently drops the rewrite. Folding Deny into `Err` would
//! conflate "the policy said no" with "the policy engine crashed" — the
//! host treats those completely differently (Deny is a normal 403, engine
//! failure is a 500 and a wakeup).
//!
//! Backend-failure errors still exist, but they live on the outer
//! [`AuthzPolicyPlugin::evaluate`] signature's companion methods
//! ([`AuthzPolicyPlugin::reload_policies`]) and in the contract documented
//! on `evaluate` itself (engines fail-closed: on internal error, return
//! [`PolicyDecision::Deny`] with the error as the reason). This keeps the
//! evaluate hot path allocation-free in the success cases — no
//! `Result::unwrap_or_else` chains at every call site.
//!
//! # Backends
//!
//! Two reference backends ship alongside this trait in `crates/bext-impls/`:
//!
//! - `bext-policy-cedar` — AWS Cedar. Rich entity/resource model,
//! designed around exactly the Allow / Deny / obligations shape.
//! - `bext-policy-cel` — Google's Common Expression Language. Simpler
//! boolean expressions over a flat attribute map; maps to Allow/Deny
//! with Mutate unused.
//!
//! An OPA/Rego backend is planned but not shipped as a reference —
//! deploying a Rego runtime is heavier than the ref-plugin bar.
//!
//! # AuthzPolicy vs Auth, in one paragraph
//!
//! [`crate::auth::AuthPlugin`] turns a bearer token or session cookie
//! into an [`crate::auth::AuthUser`] (id, scopes, attributes). It knows
//! nothing about *what* the user is trying to do — it cannot, because it
//! runs before routing. `AuthzPolicyPlugin` runs **after** routing has
//! identified the `action` and `resource`, and evaluates the per-request
//! policy against the already-authenticated principal. A site can run
//! with only Auth (scope strings are enough), only AuthzPolicy (every
//! request is anonymous but policy-gated), or both stacked (auth fills
//! `principal`, policy evaluates `action`).
use HashMap;
/// Evaluation context for a single authorization decision.
///
/// Pure POD to stay WASM-ABI friendly (same convention as
/// [`crate::feature_flag::FlagContext`] and [`crate::auth::AuthRequestContext`]).
/// The host populates this from the already-authenticated request: the
/// router sets `action` and `resource`, [`crate::auth::AuthPlugin`] fills
/// `principal` via [`crate::auth::AuthUser::user_id`], and anything the
/// policy needs beyond that (tenant id, request IP, feature flags,
/// tenant plan tier) lands in `attributes` as flat string pairs.
/// Outcome of a single policy evaluation.
///
/// See the module docs for why this is a flat enum instead of
/// `Result<(), String>`. The TL;DR: callers need to branch on three
/// *normal* outcomes (Allow / Deny / Mutate), and the host must not
/// conflate "policy said no" with "engine exploded".
/// A policy engine plugin.
///
/// The runtime holds one instance per configured backend and invokes
/// `evaluate` on every gated route. Unlike [`crate::auth::AuthPlugin`],
/// which runs once per request in the auth middleware, `AuthzPolicyPlugin`
/// runs per *decision point* — a single request may evaluate multiple
/// policies if it touches multiple resources.
///
/// # Fail-closed contract
///
/// If the engine cannot evaluate (malformed policy file, internal panic
/// unwind, missing attribute that the policy declared required),
/// implementations MUST return [`PolicyDecision::Deny`] with the error
/// string as the reason rather than allowing. Allow-on-failure is a
/// well-known authorization anti-pattern and the trait contract forbids it.
///
/// Concurrency: implementations MUST be safe to call from multiple
/// threads simultaneously. The host evaluates policies from every
/// request-handling thread without serialization.
/// Fuel budgets for WASM authz-policy plugin calls. Matches the convention
/// in [`crate::locking::fuel`] and [`crate::scheduled::fuel`].