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
//! Lookup-style enumeration for "what can this subject see?" authorization.
//!
//! The point-check API (`evaluate_in_session`) and the batch filter
//! (`filter_authorized_in_session_by_resource`) both require the caller
//! to already hold every candidate resource. That breaks down for list and
//! scope endpoints where the candidate population may be millions of rows
//! and the visible subset is tiny.
//!
//! [`LookupSource`] solves this by enumerating a *candidate superset* of
//! resources the subject may have access to, page by page; the consuming
//! [`PermissionChecker`] hydrates each page and routes the hydrated
//! resources through the existing policy stack. The lookup step is strictly
//! a **narrowing** of candidates — every policy in the checker still runs
//! on the hydrated subset, so authorization is still centralized in
//! gatehouse rather than smeared into the source's query.
//!
//! See [`LookupSource`] for the completeness contract.
//!
//! [`PermissionChecker`]: crate::PermissionChecker
use async_trait;
use fmt;
use Future;
use NonZeroUsize;
/// Enumerates a candidate superset of resources for a subject.
///
/// # Completeness contract
///
/// A `LookupSource` **must** enumerate a superset of every resource that any
/// policy in the consuming [`PermissionChecker`] could grant for `subject`.
/// Gatehouse uses lookup only to narrow the candidate set; it then runs the
/// full policy stack on the hydrated subset. **If the source omits a grant
/// path** — admin overrides, sharing relationships, global/public resources,
/// secondary roles — **the result is incomplete, not denied.** There is no
/// out-of-band signal that completeness was broken: the caller will simply
/// see fewer resources than they should.
///
/// In particular, [`PermissionChecker`] uses OR semantics across policies.
/// If you compose policies whose grant axes are independent (for example,
/// "I own it" OR "it is public" OR "the admin override applies"), the
/// `LookupSource` must enumerate the union of every axis. Lookup is the
/// scaling story for the narrow case where one axis dominates; it is
/// **not** a way to express policy logic inside the data layer.
///
/// # Cursor contract
///
/// `cursor` is opaque to gatehouse. Implementations may use any encoding
/// (offset, last-seen ID, base64 of internal state). The contract:
///
/// * `None` cursor means "start from the beginning."
/// * Return `next_cursor = None` to signal exhaustion.
/// * `next_cursor` must strictly advance: returning the same cursor that
/// was just consumed signals a stuck source and is reported by
/// gatehouse as a cursor-progress contract violation.
/// * `limit` is an upper bound on page size; pages may be shorter, but
/// shorter does not mean "exhausted" unless `next_cursor` is `None`.
///
/// # Fail-closed behavior
///
/// Returning `Err` aborts the consuming pipeline; gatehouse does not yield
/// partial results from the page.
///
/// [`PermissionChecker`]: crate::PermissionChecker
/// One page of enumerated candidate IDs.
/// Resolves enumerated IDs to caller-owned resources.
///
/// Hydration is separated from lookup so the same `LookupSource` can drive
/// different resource shapes (summary vs. full row, with or without
/// joins). The hydrator returns one `Option<Resource>` per input ID **in
/// input order**:
///
/// * `Some(resource)` — the ID resolved.
/// * `None` — the ID was enumerated by the source but no longer resolves
/// (deleted between enumeration and hydration). Gatehouse skips
/// `None` entries before running policies; this is not an error.
///
/// Returning a vector of length other than `ids.len()` is a contract
/// violation and is reported by gatehouse as a hydrator contract error.
/// Returning `Err` aborts the consuming pipeline.
///
/// Like [`crate::FactSource`], a `Hydrator` is a natural place to call an
/// existing DataLoader-style batch loader (`async_graphql::dataloader`
/// from the `async-graphql` crate, the `ultra-batch` crate, or a
/// home-grown batcher). Gatehouse hands the hydrator a candidate-page
/// slice of IDs and expects `Vec<Option<Resource>>` *in input order*;
/// most DataLoader APIs instead return a `HashMap<Id, Resource>`, so the
/// hydrator implementation re-orders the loader's output into the slice
/// shape gatehouse needs (with `None` for IDs that no longer resolve).
/// Gatehouse authorizes the resolved subset through the existing policy
/// stack; the underlying loader owns request-wide batching and caching.
/// Blanket implementation over closures, so callers can pass an `async`
/// function or a `move |ids| async move { ... }` block directly.
/// Failure modes for [`PermissionChecker::lookup_authorized`] and
/// [`PermissionChecker::lookup_authorized_page`].
///
/// The generic parameters carry the source's and hydrator's own error
/// types, so callers retain full backend context on the wrapped variants.
///
/// [`PermissionChecker::lookup_authorized`]: crate::PermissionChecker::lookup_authorized
/// [`PermissionChecker::lookup_authorized_page`]: crate::PermissionChecker::lookup_authorized_page
/// One page of *authorized* resources, paired with the next candidate-page
/// cursor.
///
/// Note that `next_cursor` paginates the **candidate** stream, not the
/// authorized output. A `Some(cursor)` value with `resources.is_empty()`
/// is normal: the source enumerated more IDs but the policy stack denied
/// every one in that page. Continue paging until `next_cursor` is `None`.