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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
//! Who may reach a declared table's rows, and which of them (issue #153).
//!
//! The manifest declares an access level per table (#359) and the harness
//! can now identify a caller (#362). This is the decision between them,
//! and it is kept apart from the handlers on purpose: it is the part that
//! decides who sees whose rows, it has no HTTP in it, and every case can
//! be stated as a value rather than as a request.
//!
//! # Refusals say as little as possible
//!
//! Under `owner`, a row belonging to somebody else is **not found**, not
//! forbidden. A `403` is an answer about a row the caller was told
//! exists, which is a fact they had no way to learn. The design produces
//! that answer by construction rather than by remembering to: the
//! subject filter is part of the same `WHERE` as the key, so the row
//! simply does not match.
//!
//! # `tenant-members` under the two tenancy shapes
//!
//! There is still no membership fact to check. A [`Caller`] is an id, a
//! session and an address; a `Subject` carries no tenant claim, and
//! `Ports::tenants` is not a [`Port`](cratefield_core::Port) and
//! `view_for` never copies it, so a module cannot ask which tenant it is
//! serving either. What a request does carry is its
//! [`Tenancy`] — which of the deployment's two shapes served it, read
//! from the `TenantConn` the resolution layer handed over and from
//! nowhere else — and the two shapes disagree about what the level would
//! be admitting.
//!
//! Under [`Tenancy::Sole`] the deployment has no registry: a venture `fz
//! build` generates has one tenant, and every verified subject is a
//! member of it because there is no second tenant to belong to. "Any
//! verified caller" and "a member of this tenant" are the same set, and
//! the level serves as it always did.
//!
//! Under [`Tenancy::FromRegistry`] a registry named the tenant from the
//! `Host` header, and the two sets come apart: the verifier is the
//! deployment's while the tenant is the host's, so a subject who signed
//! in as a user of tenant A presents the same bearer at tenant B's host
//! and "any verified caller" is a wider set than "a member of this
//! tenant". Serving the first as the second is the leak of issue #385,
//! and the fact that would close it — a tenant claim on `Subject` — does
//! not exist. So the level refuses with [`NO_MEMBERSHIP_FACT`] rather
//! than guessing who is a member. That is the whole of this: it does not
//! implement membership, it refuses where membership would be needed,
//! and #385 stays open for the real answer.
use ;
use Access;
use TableDef;
use StatusCode;
use Value;
/// A caller presented no credential where one is required — core's shared
/// 401, `cratefield_core::SLUGS.unauthenticated`. The same body answers a
/// presented credential that did not verify ([`crate::read::NOT_VERIFIED`]):
/// one 401 for every unauthenticated caller, because each distinguishable
/// answer is a hint about how to get closer.
pub const UNAUTHENTICATED: ProblemDef = SLUGS.unauthenticated;
/// A caller is signed in and still may not reach this table.
pub const FORBIDDEN: ProblemDef = ProblemDef ;
/// The declaration names a subject column the table does not have, or
/// none at all.
///
/// A manifest that would do this is refused by `fz build`
/// (`cratefield_manifest::access::validate`), so reaching this means a
/// deployment is running a composition its manifest would not produce.
/// It is a `500` and not a `403`: nothing the caller did is wrong.
pub const MISDECLARED: ProblemDef = ProblemDef ;
/// A `tenant-members` table on a deployment whose tenants come from a
/// registry (issue #385).
///
/// Where a registry resolved the tenant, "any caller the verifier
/// accepts" and "a member of this tenant" are different sets — the
/// verifier is the deployment's and the tenant is the host's — and the
/// level has no membership fact to tell them apart with. Serving anyway
/// would hand every verified caller every tenant's rows, so the level is
/// refused rather than guessed at.
///
/// It is a `500` and not a `403`: nothing the caller did is wrong, and
/// no credential of theirs fixes it — signing in as somebody else only
/// presents a different caller the deployment equally cannot place.
pub const NO_MEMBERSHIP_FACT: ProblemDef = ProblemDef ;
/// One declared table, with everything a request needs to decide about it.
/// How much of a table this request reaches.
/// Whether this caller may read this table, and which rows.
///
/// `admin` is the outcome of `cratefield_core::require_admin`, passed in
/// rather than computed here so the decision stays a function of values.
/// Its own refusal is propagated unchanged: it distinguishes "admin
/// endpoints are disabled or you sent no token" from "the token is
/// wrong", and re-deciding that here would lose the difference.
///
/// # Errors
///
/// [`UNAUTHENTICATED`] when the level needs a signed-in caller and there
/// is none, the admin check's own problem for `admin`, [`MISDECLARED`]
/// when `owner` has no subject column to match against, and
/// [`NO_MEMBERSHIP_FACT`] when the tenant came from a registry, where
/// `tenant-members` cannot be honoured for anyone.
/// A table nobody may write through this API.
pub const READ_ONLY: ProblemDef = ProblemDef ;
/// A write that would give a row to somebody other than its writer.
pub const NOT_YOURS_TO_GIVE: ProblemDef = ProblemDef ;
/// Whether this caller may write this table, and which rows.
///
/// The same shape as [`may_read`] and deliberately a separate function:
/// `public-read` reads for everybody and writes for nobody, so one
/// function answering both would need a parameter saying which — and the
/// day somebody passes the wrong one, a public table becomes writable.
///
/// # Errors
///
/// [`READ_ONLY`] for `public-read`, [`UNAUTHENTICATED`] when the level
/// needs a signed-in caller and there is none, the admin check's own
/// problem for `admin`, [`MISDECLARED`] when `owner` has no subject
/// column to match against, and [`NO_MEMBERSHIP_FACT`] when the tenant
/// came from a registry, where `tenant-members` cannot be honoured for
/// anyone.
/// The `owner` scope, shared by reads and writes because getting it
/// right twice is how the two drift apart.
/// Settles the subject column of a row about to be written.
///
/// Under [`Reach::OwnedBy`] the harness sets it, not the caller:
///
/// - absent, or null, and it is filled in with the caller's own id;
/// - already the caller's, and it is left alone;
/// - somebody else's, and the write is refused.
///
/// The third is the one worth arguing about. Overwriting silently would
/// be safe — the row would still be the caller's — but the client asked
/// for something and got something else without being told, which is how
/// a bug in a client becomes data nobody can explain. Refusing says what
/// happened.
///
/// # Errors
///
/// [`NOT_YOURS_TO_GIVE`] when the row names a different subject.