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
254
255
256
257
258
259
260
261
262
263
//! An in-process authorization engine for Rust.
//!
//! Gatehouse keeps authorization logic in Rust while giving policy code a
//! request-scoped fact session for relationship and backend-loaded data. The
//! public API is centered on one [`PolicyDomain`] marker per authorization
//! domain, a [`PermissionChecker`] that owns that domain's policy stack, and a
//! [`BoundEvaluator`] created for one request/session/subject/action/context.
//!
//! # Overview
//!
//! A [`Policy`] is an asynchronous decision unit for one [`PolicyDomain`]. The
//! domain names the four Rust types involved in a decision:
//!
//! - `Subject`: the caller.
//! - `Action`: the operation being attempted.
//! - `Resource`: the target resource or scope resource.
//! - `Context`: request-scoped inputs such as current time, MFA freshness,
//! network zone, tenant config, or feature flags.
//!
//! Relationship data and other backend-loaded authorization facts do not
//! belong in `Context`; expose them as [`FactKey`] values loaded by an
//! [`EvaluationSession`]. The session batches, deduplicates, caches, and
//! coalesces fact loads for one request.
//!
//! # Quick Start
//!
//! The fastest way to define a synchronous predicate policy is
//! [`PolicyBuilder`]:
//!
//! ```rust
//! # use gatehouse::*;
//! #[derive(Debug, Clone)]
//! struct User {
//! id: u64,
//! roles: Vec<&'static str>,
//! }
//! #[derive(Debug, Clone)]
//! struct Document {
//! owner_id: u64,
//! }
//! #[derive(Debug, Clone)]
//! struct ReadAction;
//!
//! struct Documents;
//! impl PolicyDomain for Documents {
//! type Subject = User;
//! type Action = ReadAction;
//! type Resource = Document;
//! type Context = ();
//! }
//!
//! let admin_policy = PolicyBuilder::<Documents>::new("AdminOnly")
//! .subjects(|user: &User| user.roles.contains(&"admin"))
//! .build();
//!
//! let owner_policy = PolicyBuilder::<Documents>::new("OwnerOnly")
//! .when(|user: &User, _action: &ReadAction, document: &Document, _ctx: &()| {
//! user.id == document.owner_id
//! })
//! .build();
//!
//! let mut checker = PermissionChecker::<Documents>::new();
//! checker.add_policy(admin_policy);
//! checker.add_policy(owner_policy);
//!
//! # tokio_test::block_on(async {
//! let session = EvaluationSession::empty();
//! let document = Document { owner_id: 7 };
//! let admin = User { id: 1, roles: vec!["admin"] };
//! let owner = User { id: 7, roles: vec!["user"] };
//! let guest = User { id: 2, roles: vec!["user"] };
//!
//! assert!(checker.bind(&session, &admin, &ReadAction, &()).check(&document).await.is_granted());
//! assert!(checker.bind(&session, &owner, &ReadAction, &()).check(&document).await.is_granted());
//! assert!(!checker.bind(&session, &guest, &ReadAction, &()).check(&document).await.is_granted());
//! # });
//! ```
//!
//! # Core Flows
//!
//! Bind request-wide inputs once, then evaluate resources through the bound
//! evaluator:
//!
//! ```rust,ignore
//! let session = registry.session();
//! let bound = checker.bind(&session, &subject, &action, &request_context);
//!
//! let decision = bound.check(&resource).await;
//! let decisions = bound.evaluate(resources.clone()).await;
//! let authorized = bound.filter(resources).await;
//! let authorized_rows = bound.filter_by(rows, |row| &row.authz_resource).await;
//! let page = bound.lookup_page(&lookup, &hydrator, cursor.as_deref(), limit).await?;
//! ```
//!
//! Use [`EvaluationSession::empty`] for fact-free decisions. Use a session from
//! [`FactRegistry::session`] when any policy calls `ctx.session.get(...)`, such
//! as [`RebacPolicy`] or a custom fact-backed policy.
//!
//! [`BoundEvaluator::evaluate`] preserves input order and returns one
//! [`AccessEvaluation`] per input resource. [`BoundEvaluator::filter`] keeps
//! only granted resources. [`BoundEvaluator::evaluate_by`] and
//! [`BoundEvaluator::filter_by`] are for wide caller-owned rows where
//! authorization uses a projected resource. [`BoundEvaluator::lookup_page`] is
//! for list endpoints where the application cannot load every possible
//! candidate first; the [`LookupSource`] enumerates candidate IDs, a
//! [`Hydrator`] resolves them, and the full policy stack authorizes the
//! hydrated resources.
//!
//! # Decision Semantics
//!
//! Gatehouse deliberately keeps combining semantics fixed:
//!
//! - [`PermissionChecker`] applies deny-overrides. Any evaluated result
//! containing [`PolicyEvalResult::Forbidden`] denies the request and
//! overrides grants.
//! - Policies declaring [`Effect::Forbid`] or [`Effect::AllowOrForbid`] are
//! evaluated before allow-only policies so a veto cannot be skipped by grant
//! short-circuiting.
//! - If no policy forbids, the first grant wins.
//! - If nothing grants, the checker denies with `"All policies denied access"`.
//! - An empty checker denies with `"No policies configured"`.
//! - [`PolicyEvalResult::NotApplicable`] means the policy did not grant.
//! [`PolicyEvalResult::Forbidden`] means the policy actively vetoed.
//! - [`PolicyBuilder`] combines configured predicates with AND logic.
//! [`PolicyBuilder::forbid`] makes a matching built policy forbid; a
//! non-match remains not applicable and does not block.
//! - [`AndPolicy`] and [`OrPolicy`] evaluate veto-capable children before
//! allow-only children, then short-circuit normally. [`NotPolicy`] inverts
//! grants and non-grants, but never turns `Forbidden` into a grant.
//! - `Forbidden` propagates through [`AndPolicy`], [`OrPolicy`], [`NotPolicy`],
//! and [`DelegatingPolicy`].
//! - [`NotPolicy`] does not neutralize a veto. `admin.or(blocked.not())` still
//! denies when `blocked` returns `Forbidden`. For "grant unless blocked", use
//! an allow-only `blocked` predicate under `not()`, or register an explicit
//! forbid policy when the block should be global.
//! - `grant.and(forbid_only)` can never grant: a forbid-only child does not
//! satisfy AND's "all children grant" rule. Use
//! `grant.and(blocked_allow_predicate.not())` for a local exclusion.
//!
//! Denials from [`AccessEvaluation`] are summary-level. Use
//! [`AccessEvaluation::display_trace`] or the attached [`EvalTrace`] to inspect
//! individual policy reasons and fact provenance.
//!
//! # Fact-Loaded Authorization
//!
//! [`FactSource::load_many`] receives unique fact keys and must return exactly
//! one result per key in the same order. [`EvaluationSession`] expands
//! duplicate caller inputs, preserves caller order, caches results for the
//! request, chunks loads according to [`FactSource::max_batch_size`], and joins
//! concurrent in-flight loads for the same key.
//!
//! [`RebacPolicy`] is the built-in fact-backed policy. It extracts flat
//! subject/resource IDs, builds [`RelationshipQuery`] keys, and grants only
//! when the request session loads a `Found(true)` relationship fact. Missing
//! sources, missing facts, backend errors, and fact-source contract violations
//! fail closed to denied ReBAC decisions.
//!
//! # Long-Lived Streams
//!
//! [`EvaluationSession`] caches are scoped to one authorization pass. For SSE,
//! WebSocket, and other long-lived streams, do not hold one fact-backed session
//! for the stream lifetime.
//!
//! If your product contract authorizes once at stream open, create a fresh
//! session, compute the visible ID set with [`BoundEvaluator::filter`] or
//! [`BoundEvaluator::filter_by`], drop the session, and only emit frames for
//! that set. If the stream must observe mid-stream permission revocation, run
//! periodic reauthorization with a fresh [`FactRegistry::session`] and re-bind
//! the checker for that pass.
//!
//! # Built-In Policies
//!
//! - [`RbacPolicy`]: role-based access control from caller roles and required
//! roles for the `(action, resource)` pair.
//! - [`RebacPolicy`]: relationship-based access control backed by
//! [`FactSource`] and [`EvaluationSession`].
//! - [`DelegatingPolicy`]: maps the current inputs into another
//! [`PolicyDomain`] and delegates to a child [`PermissionChecker`].
//!
//! Use [`PolicyBuilder::when`] for attribute-style predicates that compare
//! subject, action, resource, and context in one synchronous closure.
//!
//! # Custom Policies
//!
//! Implement [`Policy`] directly when a rule needs async work, custom batching,
//! custom telemetry metadata, or hand-written forbid behavior:
//!
//! ```rust
//! # use async_trait::async_trait;
//! # use std::borrow::Cow;
//! # use gatehouse::*;
//! # #[derive(Debug, Clone)] struct User { id: u64 }
//! # #[derive(Debug, Clone)] struct Document { owner_id: u64 }
//! # #[derive(Debug, Clone)] struct ReadAction;
//! # struct Documents;
//! # impl PolicyDomain for Documents {
//! # type Subject = User;
//! # type Action = ReadAction;
//! # type Resource = Document;
//! # type Context = ();
//! # }
//! struct OwnerPolicy;
//!
//! #[async_trait]
//! impl Policy<Documents> for OwnerPolicy {
//! async fn evaluate(&self, ctx: &EvalCtx<'_, Documents>) -> PolicyEvalResult {
//! if ctx.subject.id == ctx.resource.owner_id {
//! ctx.grant("subject owns the document")
//! } else {
//! ctx.not_applicable("subject does not own the document")
//! }
//! }
//!
//! fn policy_type(&self) -> Cow<'static, str> {
//! Cow::Borrowed("OwnerPolicy")
//! }
//! }
//! ```
//!
//! # Tracing
//!
//! When trace-level events are enabled, checker evaluation records spans for
//! single-resource and batch evaluation, and each evaluated policy records a
//! `trace!` event on the `gatehouse::security` target. Batch evaluation also
//! records per-policy counts on nested `gatehouse.batch_policy` spans.
pub use PolicyBuilder;
pub use ;
pub use ;
pub use ;
pub use ;
pub use SecurityRuleMetadata;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
// The shared unit-test module pulls in tokio-based async tests via dev-deps
// that are intentionally loom-incompatible (`tokio::net`, axum, hyper, etc.).
// Gate it out under `cfg(loom)` so the loom build's minimal dependency graph
// stays clean. The synchronous core's deterministic tests and the loom
// permutation tests both live in `src/session/core.rs` and are unaffected.