@stonyx/orm 0.3.2-alpha.62 → 0.3.2-alpha.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -20
- package/dist/orm-request.d.ts +17 -5
- package/dist/orm-request.js +17 -5
- package/package.json +1 -1
- package/src/orm-request.ts +17 -5
package/README.md
CHANGED
|
@@ -350,19 +350,24 @@ export default class GlobalAccess {
|
|
|
350
350
|
// `model` is the model this route was mounted for. It is assigned once, at
|
|
351
351
|
// mount time, and no request can influence it — not a mount prefix, not a
|
|
352
352
|
// query string, not a case-varied path, not an absolute-form request
|
|
353
|
-
// target. Nothing below parses anything, so
|
|
354
|
-
//
|
|
355
|
-
//
|
|
353
|
+
// target. Nothing below parses anything, so variants 1, 2, 4 and 5 are not
|
|
354
|
+
// constructible against this predicate any more — they are history, not
|
|
355
|
+
// rules to follow. VARIANT 3 IS THE EXCEPTION AND THE CLAIM IS NARROWER
|
|
356
|
+
// THAN IT WAS: a matcher stricter than the router can still be stepped
|
|
357
|
+
// around, because the sub-path rule below is still a string comparison.
|
|
358
|
+
// Case is handled; percent-encoding is not — abofs/stonyx-orm#228.
|
|
356
359
|
//
|
|
357
360
|
// `operation` is destructured to name the whole contract at the point of
|
|
358
361
|
// use. This sample's rules are per-model and per-sub-path rather than
|
|
359
362
|
// per-verb, so it does not branch on it; the permission array at the bottom
|
|
360
363
|
// is where the verb is answered.
|
|
361
364
|
|
|
362
|
-
// FAIL CLOSED. `model` is absent for any caller that
|
|
363
|
-
// predicate without supplying the context, and a request this
|
|
364
|
-
// cannot identify DENIES rather than falling through to the CRUD
|
|
365
|
-
// the bottom. An unidentifiable input must never be the permissive
|
|
365
|
+
// FAIL CLOSED ON ARGUMENT TWO. `model` is absent for any caller that
|
|
366
|
+
// resolved this predicate without supplying the context, and a request this
|
|
367
|
+
// function cannot identify DENIES rather than falling through to the CRUD
|
|
368
|
+
// grant at the bottom. An unidentifiable input must never be the permissive
|
|
369
|
+
// path. Argument ONE is guarded at its own read, below — this guard does
|
|
370
|
+
// not cover it.
|
|
366
371
|
if (typeof model !== 'string' || model === '') return false;
|
|
367
372
|
|
|
368
373
|
if (model === 'owner') {
|
|
@@ -370,14 +375,35 @@ export default class GlobalAccess {
|
|
|
370
375
|
// distinct owner surfaces produce one identical context, so a rule that
|
|
371
376
|
// depends on the SUB-PATH still needs argument one. `request.path` is
|
|
372
377
|
// mount-relative and query-free, and it is the one read of the raw
|
|
373
|
-
// request the README sanctions.
|
|
374
|
-
// case-insensitively, so a case-sensitive rule here would be stricter
|
|
375
|
-
// than the router and could be stepped around. false → 403 for the whole
|
|
376
|
-
// request.
|
|
378
|
+
// request the README sanctions. false → 403 for the whole request.
|
|
377
379
|
//
|
|
378
380
|
// THIS DENY CANNOT BE EXPRESSED FROM THE CONTEXT ALONE. Migrating it away
|
|
379
381
|
// does not remove a rule, it turns a deny into an ALLOW, silently.
|
|
380
|
-
|
|
382
|
+
//
|
|
383
|
+
// FAIL CLOSED ON ARGUMENT ONE TOO. The guard above covers the context;
|
|
384
|
+
// this one covers the request, and since #202 they are two different
|
|
385
|
+
// objects. A caller that resolves this predicate through the documented
|
|
386
|
+
// `Orm.instance.getAccess()` path and hand-assembles a request can supply
|
|
387
|
+
// a perfectly valid context with no usable `path` — and
|
|
388
|
+
// `String(request.path ?? '')` is then `''`, which matches no sub-path
|
|
389
|
+
// rule and falls straight through to the per-record filter below. That is
|
|
390
|
+
// a DENY becoming an ALLOW. An input this function cannot identify DENIES,
|
|
391
|
+
// whichever ARGUMENT it arrived on — which is also why the `?? ''` this
|
|
392
|
+
// file's header condemns does not appear below.
|
|
393
|
+
if (typeof request?.path !== 'string' || request.path === '') return false;
|
|
394
|
+
|
|
395
|
+
// Lower-cased because the router matched case-insensitively, so a
|
|
396
|
+
// case-sensitive rule here would be stricter than the router and could be
|
|
397
|
+
// stepped around.
|
|
398
|
+
//
|
|
399
|
+
// CASE-FOLDING ALONE IS NOT A SUFFICIENT NORMALISATION, and this line is
|
|
400
|
+
// not a recipe for one. Express sets `request.path` from the RAW pathname
|
|
401
|
+
// while the router DECODES `:id`, so `GET /owners/%61rchived` reaches this
|
|
402
|
+
// comparison as `/%61rchived`, walks past the deny, and is dispatched as
|
|
403
|
+
// the record `archived` — abofs/stonyx-orm#228. A matcher must normalise
|
|
404
|
+
// the way the router that dispatched the request does. Record ids are
|
|
405
|
+
// case-sensitive and must be compared at their real case.
|
|
406
|
+
const path = request.path.toLowerCase();
|
|
381
407
|
|
|
382
408
|
if (path === '/archived' || path.startsWith('/archived/')) return false;
|
|
383
409
|
|
|
@@ -672,7 +698,12 @@ variant was found only after the previous one was fixed:
|
|
|
672
698
|
| 5 | any match on `originalUrl` at all | HTTP/1.1 permits an **absolute-form** request-target. Express routes on `parseurl(req).pathname`, so the request dispatches normally — but `originalUrl` is the raw target. `GET http://anything.example/owners/angela` yields `originalUrl === 'http://anything.example/owners/angela'`, which has no `/owners` prefix. Measured: the record came back in full, `DELETE` succeeded, and it walked past a hard `return false` deny the same way. |
|
|
673
699
|
|
|
674
700
|
**The fix is not a sixth rule, and it is not a better string to match.** It is
|
|
675
|
-
to stop identifying the collection at all.
|
|
701
|
+
to stop identifying the collection at all. That is a statement about
|
|
702
|
+
**identifying the collection**, and it is not a statement about the sample as a
|
|
703
|
+
whole: the `/archived` sub-path rule *is* still a string match, and
|
|
704
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) is a sixth spelling that
|
|
705
|
+
gets past it. Sub-path rules are the residue this fix does not cover, which is
|
|
706
|
+
why they must normalise the way the router does.
|
|
676
707
|
|
|
677
708
|
An intermediate revision read **`request.baseUrl`** — the mount Express
|
|
678
709
|
*actually matched*. That closed all five variants: it carries no query string
|
|
@@ -699,18 +730,30 @@ mount-relative and query-free, and it is for rules that distinguish **sub-paths*
|
|
|
699
730
|
beneath the mount — as the `/archived` deny in the sample above does. The context
|
|
700
731
|
names which model and which verb, **not which route**, so that deny *cannot be
|
|
701
732
|
expressed from the context alone*, and a context-only rewrite would silently turn
|
|
702
|
-
it into an allow.
|
|
703
|
-
|
|
704
|
-
|
|
733
|
+
it into an allow.
|
|
734
|
+
|
|
735
|
+
**Normalise the way the router that dispatched the request does — and
|
|
736
|
+
case-folding alone does not.** A matcher stricter than the router can be stepped
|
|
737
|
+
around, so the sample lower-cases before comparing (the router matched
|
|
738
|
+
case-insensitively). That closes the case gap and **it is not the whole rule**:
|
|
739
|
+
Express sets `request.path` from the **raw, undecoded** pathname while the router
|
|
740
|
+
**decodes** `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
741
|
+
comparison as `/%61rchived`, walks past the deny, and is dispatched as the record
|
|
742
|
+
`archived`. That gap is live in the sample above and is tracked as
|
|
743
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) — **do not read the
|
|
744
|
+
`.toLowerCase()` there as a complete normalisation recipe.** Record ids are
|
|
705
745
|
case-sensitive and must be compared at their real case.
|
|
706
746
|
|
|
707
|
-
**Fail closed on anything you cannot identify.**
|
|
747
|
+
**Fail closed on anything you cannot identify — on *either* argument.**
|
|
708
748
|
`String(request.originalUrl ?? '')` was once added here to stop a `TypeError`,
|
|
709
749
|
and it traded fail-closed for fail-**open**: an empty string matched no
|
|
710
750
|
collection, so `access()` fell through to the permission array and granted full
|
|
711
|
-
CRUD. The same rule
|
|
712
|
-
|
|
713
|
-
**
|
|
751
|
+
CRUD. The same rule applies to the context — the sample returns `false` for an
|
|
752
|
+
absent `model` rather than falling through. Since #202 the guard and the read can
|
|
753
|
+
sit on **different objects**, and a guard on argument two does not protect a read
|
|
754
|
+
of argument one: the sample therefore also returns `false` when `request.path` is
|
|
755
|
+
absent or is not a string, rather than letting `?? ''` fall through to the
|
|
756
|
+
per-record filter. An input you cannot identify must **deny**.
|
|
714
757
|
|
|
715
758
|
### Known limitations
|
|
716
759
|
|
package/dist/orm-request.d.ts
CHANGED
|
@@ -153,7 +153,10 @@
|
|
|
153
153
|
* `return false` deny the same way.
|
|
154
154
|
*
|
|
155
155
|
* The fix is not a sixth rule, and it is not a better string to match. It is to
|
|
156
|
-
* stop identifying the collection at all: read `context.model`.
|
|
156
|
+
* stop identifying the collection at all: read `context.model`. That is a claim
|
|
157
|
+
* about IDENTIFYING THE COLLECTION, not about the sample as a whole -- the
|
|
158
|
+
* `/archived` SUB-PATH rule is still a string match, and abofs/stonyx-orm#228 is
|
|
159
|
+
* a sixth spelling that gets past it.
|
|
157
160
|
*
|
|
158
161
|
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
159
162
|
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
@@ -168,14 +171,23 @@
|
|
|
168
171
|
* beneath the mount. The context names which model and which verb, NOT which
|
|
169
172
|
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
170
173
|
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
171
|
-
*
|
|
172
|
-
*
|
|
174
|
+
*
|
|
175
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
176
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
177
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
178
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
179
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
180
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
181
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
182
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
173
183
|
*
|
|
174
184
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
175
185
|
* string, which matches no collection, which falls through to the permission
|
|
176
186
|
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
177
|
-
* applies to
|
|
178
|
-
*
|
|
187
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
188
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
189
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
190
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
179
191
|
*
|
|
180
192
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
181
193
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|
package/dist/orm-request.js
CHANGED
|
@@ -153,7 +153,10 @@
|
|
|
153
153
|
* `return false` deny the same way.
|
|
154
154
|
*
|
|
155
155
|
* The fix is not a sixth rule, and it is not a better string to match. It is to
|
|
156
|
-
* stop identifying the collection at all: read `context.model`.
|
|
156
|
+
* stop identifying the collection at all: read `context.model`. That is a claim
|
|
157
|
+
* about IDENTIFYING THE COLLECTION, not about the sample as a whole -- the
|
|
158
|
+
* `/archived` SUB-PATH rule is still a string match, and abofs/stonyx-orm#228 is
|
|
159
|
+
* a sixth spelling that gets past it.
|
|
157
160
|
*
|
|
158
161
|
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
159
162
|
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
@@ -168,14 +171,23 @@
|
|
|
168
171
|
* beneath the mount. The context names which model and which verb, NOT which
|
|
169
172
|
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
170
173
|
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
171
|
-
*
|
|
172
|
-
*
|
|
174
|
+
*
|
|
175
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
176
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
177
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
178
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
179
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
180
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
181
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
182
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
173
183
|
*
|
|
174
184
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
175
185
|
* string, which matches no collection, which falls through to the permission
|
|
176
186
|
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
177
|
-
* applies to
|
|
178
|
-
*
|
|
187
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
188
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
189
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
190
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
179
191
|
*
|
|
180
192
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
181
193
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|
package/package.json
CHANGED
package/src/orm-request.ts
CHANGED
|
@@ -153,7 +153,10 @@
|
|
|
153
153
|
* `return false` deny the same way.
|
|
154
154
|
*
|
|
155
155
|
* The fix is not a sixth rule, and it is not a better string to match. It is to
|
|
156
|
-
* stop identifying the collection at all: read `context.model`.
|
|
156
|
+
* stop identifying the collection at all: read `context.model`. That is a claim
|
|
157
|
+
* about IDENTIFYING THE COLLECTION, not about the sample as a whole -- the
|
|
158
|
+
* `/archived` SUB-PATH rule is still a string match, and abofs/stonyx-orm#228 is
|
|
159
|
+
* a sixth spelling that gets past it.
|
|
157
160
|
*
|
|
158
161
|
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
159
162
|
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
@@ -168,14 +171,23 @@
|
|
|
168
171
|
* beneath the mount. The context names which model and which verb, NOT which
|
|
169
172
|
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
170
173
|
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
171
|
-
*
|
|
172
|
-
*
|
|
174
|
+
*
|
|
175
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
176
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
177
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
178
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
179
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
180
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
181
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
182
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
173
183
|
*
|
|
174
184
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
175
185
|
* string, which matches no collection, which falls through to the permission
|
|
176
186
|
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
177
|
-
* applies to
|
|
178
|
-
*
|
|
187
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
188
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
189
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
190
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
179
191
|
*
|
|
180
192
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
181
193
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|