@stonyx/orm 0.3.2-beta.155 → 0.3.2-beta.156
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 +168 -69
- package/dist/orm-request.d.ts +60 -25
- package/dist/orm-request.js +58 -23
- package/package.json +1 -1
- package/src/orm-request.ts +58 -23
package/README.md
CHANGED
|
@@ -315,15 +315,28 @@ Access classes define models and provide custom filtering/authorization logic.
|
|
|
315
315
|
> [Identifying the collection](#identifying-the-collection) before copying this.**
|
|
316
316
|
> Every attempt to identify the collection by parsing the request target has
|
|
317
317
|
> failed **open** — five distinct variants of this same example, each found only
|
|
318
|
-
> after the previous was fixed, by five different people.
|
|
319
|
-
> not
|
|
320
|
-
>
|
|
318
|
+
> after the previous was fixed, by five different people. That section is now a
|
|
319
|
+
> record of what not to do, not a matching recipe: the sample below reads `model`
|
|
320
|
+
> from [the access context](#the-access-context-second-argument) and never looks
|
|
321
|
+
> at the mount at all, so variants 1, 2, 4 and 5 are **unconstructible** against
|
|
322
|
+
> it rather than merely handled. **Variant 3 survives.** It is the general shape
|
|
323
|
+
> "a hand-written matcher normalises differently from the router", and the
|
|
324
|
+
> migrated sample still runs one string comparison — the `/archived` sub-path
|
|
325
|
+
> deny — which folds case but does not decode, so `GET /owners/%61rchived` steps
|
|
326
|
+
> past it ([#228](https://github.com/abofs/stonyx-orm/issues/228)).
|
|
321
327
|
>
|
|
322
328
|
> That is still a stopgap. **The real fix is
|
|
323
329
|
> [#202](https://github.com/abofs/stonyx-orm/issues/202)** — `access()` should
|
|
324
330
|
> receive the model, the operation and the record, so there is nothing to
|
|
325
331
|
> identify. Until it lands, prefer the array shape (`['read']`) or `false` where
|
|
326
332
|
> you can: the **function** shape is the one that requires any matching at all.
|
|
333
|
+
>
|
|
334
|
+
> The one read of argument **one** that survives is `request.path`, for the
|
|
335
|
+
> `/archived` sub-path deny — and it has to. The context names which model and
|
|
336
|
+
> which verb, not which route, so that deny **cannot be expressed from the
|
|
337
|
+
> context alone** and a context-only rewrite would silently turn it into an
|
|
338
|
+
> allow.
|
|
339
|
+
>
|
|
327
340
|
> The same warning is repeated at the top of `src/orm-request.ts`, which ships;
|
|
328
341
|
> the longer write-up in `docs/usage-patterns.md` does **not** ship, so this
|
|
329
342
|
> README and that source header are the two copies a consumer sees.
|
|
@@ -337,27 +350,65 @@ Access classes define models and provide custom filtering/authorization logic.
|
|
|
337
350
|
export default class GlobalAccess {
|
|
338
351
|
models = ['owner', 'animal'];
|
|
339
352
|
|
|
340
|
-
access(request) {
|
|
341
|
-
// `
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
353
|
+
access(request, { model, operation }) {
|
|
354
|
+
// `model` is the model this route was mounted for. It is assigned once, at
|
|
355
|
+
// mount time, and no request can influence it — not a mount prefix, not a
|
|
356
|
+
// query string, not a case-varied path, not an absolute-form request
|
|
357
|
+
// target. Nothing below parses anything, so variants 1, 2, 4 and 5 are not
|
|
358
|
+
// constructible against this predicate any more — they are history, not
|
|
359
|
+
// rules to follow. VARIANT 3 IS THE EXCEPTION AND THE CLAIM IS NARROWER
|
|
360
|
+
// THAN IT WAS: a matcher stricter than the router can still be stepped
|
|
361
|
+
// around, because the sub-path rule below is still a string comparison.
|
|
362
|
+
// Case is handled; percent-encoding is not — abofs/stonyx-orm#228.
|
|
363
|
+
//
|
|
364
|
+
// `operation` is destructured to name the whole contract at the point of
|
|
365
|
+
// use. This sample's rules are per-model and per-sub-path rather than
|
|
366
|
+
// per-verb, so it does not branch on it; the permission array at the bottom
|
|
367
|
+
// is where the verb is answered.
|
|
368
|
+
|
|
369
|
+
// FAIL CLOSED ON ARGUMENT TWO. `model` is absent for any caller that
|
|
370
|
+
// resolved this predicate without supplying the context, and a request this
|
|
371
|
+
// function cannot identify DENIES rather than falling through to the CRUD
|
|
372
|
+
// grant at the bottom. An unidentifiable input must never be the permissive
|
|
373
|
+
// path. Argument ONE is guarded at its own read, below — this guard does
|
|
374
|
+
// not cover it.
|
|
375
|
+
if (typeof model !== 'string' || model === '') return false;
|
|
376
|
+
|
|
377
|
+
if (model === 'owner') {
|
|
378
|
+
// The context names WHICH MODEL and WHICH VERB — not which route. Six
|
|
379
|
+
// distinct owner surfaces produce one identical context, so a rule that
|
|
380
|
+
// depends on the SUB-PATH still needs argument one. `request.path` is
|
|
381
|
+
// mount-relative and query-free, and it is the one read of the raw
|
|
382
|
+
// request the README sanctions. false → 403 for the whole request.
|
|
383
|
+
//
|
|
384
|
+
// THIS DENY CANNOT BE EXPRESSED FROM THE CONTEXT ALONE. Migrating it away
|
|
385
|
+
// does not remove a rule, it turns a deny into an ALLOW, silently.
|
|
386
|
+
//
|
|
387
|
+
// FAIL CLOSED ON ARGUMENT ONE TOO. The guard above covers the context;
|
|
388
|
+
// this one covers the request, and since #202 they are two different
|
|
389
|
+
// objects. A caller that resolves this predicate through the documented
|
|
390
|
+
// `Orm.instance.getAccess()` path and hand-assembles a request can supply
|
|
391
|
+
// a perfectly valid context with no usable `path` — and
|
|
392
|
+
// `String(request.path ?? '')` is then `''`, which matches no sub-path
|
|
393
|
+
// rule and falls straight through to the per-record filter below. That is
|
|
394
|
+
// a DENY becoming an ALLOW. An input this function cannot identify DENIES,
|
|
395
|
+
// whichever ARGUMENT it arrived on — which is also why the `?? ''` this
|
|
396
|
+
// file's header condemns does not appear below.
|
|
397
|
+
if (typeof request?.path !== 'string' || request.path === '') return false;
|
|
398
|
+
|
|
399
|
+
// Lower-cased because the router matched case-insensitively, so a
|
|
400
|
+
// case-sensitive rule here would be stricter than the router and could be
|
|
401
|
+
// stepped around.
|
|
402
|
+
//
|
|
403
|
+
// CASE-FOLDING ALONE IS NOT A SUFFICIENT NORMALISATION, and this line is
|
|
404
|
+
// not a recipe for one. Express sets `request.path` from the RAW pathname
|
|
405
|
+
// while the router DECODES `:id`, so `GET /owners/%61rchived` reaches this
|
|
406
|
+
// comparison as `/%61rchived`, walks past the deny, and is dispatched as
|
|
407
|
+
// the record `archived` — abofs/stonyx-orm#228. A matcher must normalise
|
|
408
|
+
// the way the router that dispatched the request does. Record ids are
|
|
409
|
+
// case-sensitive and must be compared at their real case.
|
|
410
|
+
const path = request.path.toLowerCase();
|
|
359
411
|
|
|
360
|
-
if (collection.endsWith('/owners')) {
|
|
361
412
|
if (path === '/archived' || path.startsWith('/archived/')) return false;
|
|
362
413
|
|
|
363
414
|
// Returning a function plugs it in as a per-record filter, and it is
|
|
@@ -373,7 +424,7 @@ export default class GlobalAccess {
|
|
|
373
424
|
// inert. Deliberately NO `?? record.owner` fallback: accepting the raw
|
|
374
425
|
// shape as well as the resolved one would absorb a resolution regression
|
|
375
426
|
// silently, which is exactly what blinded this fixture before.
|
|
376
|
-
if (
|
|
427
|
+
if (model === 'animal') return record => record.owner?.id !== 'restricted';
|
|
377
428
|
|
|
378
429
|
// Allows full access to all calls that don't match any of the above conditions
|
|
379
430
|
return ['read', 'create', 'update', 'delete'];
|
|
@@ -523,9 +574,10 @@ sample, `getAccess('owner') === getAccess('animal')`.
|
|
|
523
574
|
#### Passing the context makes a model-correct answer *possible*
|
|
524
575
|
|
|
525
576
|
It does not make the answer model-correct on its own. **The resolved predicate
|
|
526
|
-
has to read the context.**
|
|
527
|
-
|
|
528
|
-
**animals
|
|
577
|
+
has to read the context.** Against a predicate that ignores it the failure is
|
|
578
|
+
measurable. On a request Express dispatched to `GET /owners/angela`, asked about
|
|
579
|
+
**animals**, the sample as it shipped before
|
|
580
|
+
[#222](https://github.com/abofs/stonyx-orm/issues/222) answered:
|
|
529
581
|
|
|
530
582
|
```
|
|
531
583
|
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
@@ -533,18 +585,26 @@ getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
|
533
585
|
```
|
|
534
586
|
|
|
535
587
|
That is the **owners** filter, and it returns `true` for animal 21 — the record
|
|
536
|
-
hidden on every animal surface. Under a mount
|
|
537
|
-
way it is worse still: it falls through to
|
|
538
|
-
`['read', 'create', 'update', 'delete']`, a full CRUD grant.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
588
|
+
hidden on every animal surface. Under a mount such a predicate recognizes
|
|
589
|
+
neither way it is worse still: it falls through to
|
|
590
|
+
`['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
|
|
591
|
+
context was supplied and the answer is not the animal answer, and it is wrong in
|
|
592
|
+
the direction that **grants** — because that predicate was single-argument and
|
|
593
|
+
identified its collection from the request, so it answered about the collection
|
|
594
|
+
the request was *addressed to* while being asked about another one.
|
|
595
|
+
|
|
596
|
+
The sample shipped with this repo has since been migrated to read the context,
|
|
597
|
+
and the same call now answers with the **animal** filter:
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
601
|
+
-> record => record.owner?.id !== 'restricted'
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
A single-argument predicate remains the default in every consumer tree, and a
|
|
605
|
+
caller has no supported way to tell which kind it resolved. The boot-time arity
|
|
606
|
+
warning that surfaces one is
|
|
607
|
+
[#221](https://github.com/abofs/stonyx-orm/issues/221).
|
|
548
608
|
|
|
549
609
|
So: pass the context, and do not treat a resolved predicate's answer as
|
|
550
610
|
model-specific until that predicate has been migrated to read it.
|
|
@@ -676,9 +736,17 @@ no hidden records to disclose.
|
|
|
676
736
|
|
|
677
737
|
### Identifying the collection
|
|
678
738
|
|
|
679
|
-
**Do not reconstruct the request path
|
|
680
|
-
|
|
681
|
-
|
|
739
|
+
**Do not reconstruct the request path — and since
|
|
740
|
+
[#202](https://github.com/abofs/stonyx-orm/issues/202) you do not have to
|
|
741
|
+
identify the collection at all.** Read `model` from
|
|
742
|
+
[the access context](#the-access-context-second-argument): it is fixed at mount
|
|
743
|
+
time, no request can influence it, and there is nothing left to parse.
|
|
744
|
+
|
|
745
|
+
**Everything below is the record of what happened when this sample did parse
|
|
746
|
+
it.** It is kept as history, not as a recipe — none of these matching strategies
|
|
747
|
+
should be written into a new predicate. Every version of this sample that tried
|
|
748
|
+
to identify the collection from the request target failed **open**, and each
|
|
749
|
+
variant was found only after the previous one was fixed:
|
|
682
750
|
|
|
683
751
|
| # | Variant | Why it fails open |
|
|
684
752
|
|---|---|---|
|
|
@@ -688,13 +756,27 @@ was fixed:
|
|
|
688
756
|
| 4 | hard-coded `/owners` | With `ORM_REST_ROUTE=/api` every url becomes `/api/owners/...` and the sample matches nothing — environment-specifically, which is harder to notice than failing everywhere. The remediation this document used to give was itself broken: `` `${config.orm.restServer.route}owners` `` evaluates to **`/apiowners`**, so a reader who followed the correction exactly still failed open and believed they had handled it. |
|
|
689
757
|
| 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. |
|
|
690
758
|
|
|
691
|
-
**The fix is not a sixth rule
|
|
692
|
-
|
|
693
|
-
**
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
759
|
+
**The fix is not a sixth rule, and it is not a better string to match.** It is
|
|
760
|
+
to stop identifying the collection at all. That is a statement about
|
|
761
|
+
**identifying the collection**, and it is not a statement about the sample as a
|
|
762
|
+
whole: the `/archived` sub-path rule *is* still a string match, and
|
|
763
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) is a sixth spelling that
|
|
764
|
+
gets past it. Sub-path rules are the residue this fix does not cover, which is
|
|
765
|
+
why they must normalise the way the router does.
|
|
766
|
+
|
|
767
|
+
An intermediate revision read **`request.baseUrl`** — the mount Express
|
|
768
|
+
*actually matched*. That closed all five variants: it carries no query string
|
|
769
|
+
(variant 2), it is not mount-relative (variant 1), it already contains the
|
|
770
|
+
configured `ORM_REST_ROUTE` prefix (variant 4 — there is nothing left to derive,
|
|
771
|
+
so `/apiowners` is unconstructible), and it is unaffected by an absolute-form
|
|
772
|
+
target (variant 5). It was still a transport artifact standing in for a
|
|
773
|
+
structural fact, and it is **no longer what the sample does**: the sample reads
|
|
774
|
+
`model`, so variants 1, 2, 4 and 5 are unconstructible against it rather than
|
|
775
|
+
handled. **Variant 3 survives**, in the one string comparison the migration
|
|
776
|
+
leaves behind: the `/archived` sub-path deny folds case but does not decode
|
|
777
|
+
([#228](https://github.com/abofs/stonyx-orm/issues/228)). The table below is
|
|
778
|
+
retained as the measured evidence behind the five variants, not because any of
|
|
779
|
+
these values should be matched on:
|
|
698
780
|
|
|
699
781
|
| request | `request.url` | `request.originalUrl` | `request.baseUrl` | `request.path` |
|
|
700
782
|
|---|---|---|---|---|
|
|
@@ -705,21 +787,35 @@ unconstructible), and it is unaffected by an absolute-form target (variant 5).
|
|
|
705
787
|
| `GET http://anything.example/owners/angela` | `http://anything.example/angela` | `http://anything.example/owners/angela` | `/owners` | `/angela` |
|
|
706
788
|
| `GET /api/animals/22` (`ORM_REST_ROUTE=/api`) | `/22` | `/api/animals/22` | `/api/animals` | `/22` |
|
|
707
789
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
790
|
+
**One read of argument one survives, and it must: `request.path`.** It is
|
|
791
|
+
mount-relative and query-free, and it is for rules that distinguish **sub-paths**
|
|
792
|
+
beneath the mount — as the `/archived` deny in the sample above does. The context
|
|
793
|
+
names which model and which verb, **not which route**, so that deny *cannot be
|
|
794
|
+
expressed from the context alone*, and a context-only rewrite would silently turn
|
|
795
|
+
it into an allow.
|
|
796
|
+
|
|
797
|
+
**Normalise the way the router that dispatched the request does — and
|
|
798
|
+
case-folding alone does not.** A matcher stricter than the router can be stepped
|
|
799
|
+
around, so the sample lower-cases before comparing (the router matched
|
|
800
|
+
case-insensitively). That closes the case gap and **it is not the whole rule**:
|
|
801
|
+
Express sets `request.path` from the **raw, undecoded** pathname while the router
|
|
802
|
+
**decodes** `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
803
|
+
comparison as `/%61rchived`, walks past the deny, and is dispatched as the record
|
|
804
|
+
`archived`. That gap is live in the sample above and is tracked as
|
|
805
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) — **do not read the
|
|
806
|
+
`.toLowerCase()` there as a complete normalisation recipe.** Record ids are
|
|
807
|
+
case-sensitive and must be compared at their real case.
|
|
808
|
+
|
|
809
|
+
**Fail closed on anything you cannot identify — on *either* argument.**
|
|
810
|
+
`String(request.originalUrl ?? '')` was once added here to stop a `TypeError`,
|
|
811
|
+
and it traded fail-closed for fail-**open**: an empty string matched no
|
|
812
|
+
collection, so `access()` fell through to the permission array and granted full
|
|
813
|
+
CRUD. The same rule applies to the context — the sample returns `false` for an
|
|
814
|
+
absent `model` rather than falling through. Since #202 the guard and the read can
|
|
815
|
+
sit on **different objects**, and a guard on argument two does not protect a read
|
|
816
|
+
of argument one: the sample therefore also returns `false` when `request.path` is
|
|
817
|
+
absent or is not a string, rather than letting `?? ''` fall through to the
|
|
818
|
+
per-record filter. An input you cannot identify must **deny**.
|
|
723
819
|
|
|
724
820
|
### Known limitations
|
|
725
821
|
|
|
@@ -742,11 +838,14 @@ sub-paths beneath the mount, as the `/archived` deny above does.
|
|
|
742
838
|
see [The access context](#the-access-context-second-argument):
|
|
743
839
|
`Orm.instance.getAccess(modelName)` makes another model's predicate
|
|
744
840
|
**reachable**, and `context.model` makes a **model-correct answer possible** —
|
|
745
|
-
possible, not guaranteed: the resolved predicate has to read the context
|
|
746
|
-
|
|
747
|
-
([#
|
|
748
|
-
|
|
749
|
-
|
|
841
|
+
possible, not guaranteed: the resolved predicate has to read the context. The
|
|
842
|
+
sample shipped with this repo now does
|
|
843
|
+
([#222](https://github.com/abofs/stonyx-orm/issues/222)), so
|
|
844
|
+
`getAccess('animal')` answers with the animal filter; a predicate that ignores
|
|
845
|
+
the second argument still answers about the collection the request is
|
|
846
|
+
addressed to, and the boot-time warning that surfaces one is
|
|
847
|
+
[#221](https://github.com/abofs/stonyx-orm/issues/221). **The mechanism
|
|
848
|
+
exists; the ORM does not yet use it on this path.** The re-parenting write above is still
|
|
750
849
|
**not refused** — that enforcement is
|
|
751
850
|
[#196](https://github.com/abofs/stonyx-orm/issues/196) and
|
|
752
851
|
[#207](https://github.com/abofs/stonyx-orm/issues/207), which were blocked on
|
package/dist/orm-request.d.ts
CHANGED
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
*
|
|
99
99
|
* PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
|
|
100
100
|
* the answer model-correct on its own -- the resolved predicate has to READ it.
|
|
101
|
-
* Measured against
|
|
102
|
-
*
|
|
101
|
+
* Measured against an ARITY-1 predicate, on a request express dispatched to
|
|
102
|
+
* `GET /owners/angela`, asked about ANIMALS:
|
|
103
103
|
*
|
|
104
104
|
* getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
105
105
|
* -> record => record.id !== 'angela' && record.id !== 'restricted'
|
|
@@ -110,23 +110,30 @@
|
|
|
110
110
|
* `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
|
|
111
111
|
* context was supplied and the answer is not the animal answer, and it is wrong
|
|
112
112
|
* in the GRANTING direction, because that predicate is arity-1 and identifies
|
|
113
|
-
* its collection from the request. (
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* the
|
|
113
|
+
* its collection from the request. (Asserted on a live dispatch by AC9 in
|
|
114
|
+
* test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
|
|
115
|
+
*
|
|
116
|
+
* This repo's own sample access class has since been MIGRATED to read the
|
|
117
|
+
* context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
|
|
118
|
+
* with the animal filter. That is not true of a consumer tree: an arity-1
|
|
119
|
+
* predicate keeps working -- the second argument is additive -- and the caller
|
|
120
|
+
* has no supported way to tell which kind it got. The boot-time arity warning
|
|
121
|
+
* that surfaces one is abofs/stonyx-orm#221.
|
|
119
122
|
* So: pass the context, and do not treat a resolved predicate's answer as
|
|
120
123
|
* model-specific until that predicate has been migrated to read the context.
|
|
121
124
|
*
|
|
122
125
|
* ---------------------------------------------------------------------------
|
|
123
126
|
* DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
|
|
124
127
|
* ---------------------------------------------------------------------------
|
|
125
|
-
* `auth()` below hands your
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
|
|
129
|
+
* argument two, and `context.model` already names the collection -- see the
|
|
130
|
+
* contract section above. Argument ONE is still the raw transport artifact, and
|
|
131
|
+
* everything from here to the end of this banner is the record of what happened
|
|
132
|
+
* when predicates worked the collection out from it. IT IS HISTORY, NOT
|
|
133
|
+
* GUIDANCE: do not write any of it into a new predicate. Every attempt to
|
|
134
|
+
* identify the collection by parsing the request target has failed OPEN. Five
|
|
135
|
+
* distinct variants of the same three-line example have now been found, each
|
|
136
|
+
* after the previous was fixed, by five different people:
|
|
130
137
|
*
|
|
131
138
|
* 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
|
|
132
139
|
* prefix match against it is ALWAYS false.
|
|
@@ -145,22 +152,50 @@
|
|
|
145
152
|
* last, and the record comes back in full. It walks past a hard
|
|
146
153
|
* `return false` deny the same way.
|
|
147
154
|
*
|
|
148
|
-
* The fix is not a sixth rule. It is to
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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`. 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.
|
|
160
|
+
*
|
|
161
|
+
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
162
|
+
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
163
|
+
* not mount-relative, unaffected by absolute-form, already carrying the
|
|
164
|
+
* configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
|
|
165
|
+
* standing in for a structural fact and the sample no longer does it.
|
|
166
|
+
* `context.model` IS the structural fact, so variants 1, 2, 4 and 5 are
|
|
167
|
+
* unconstructible against a migrated predicate rather than handled.
|
|
168
|
+
*
|
|
169
|
+
* VARIANT 3 SURVIVES, and is deliberately not in that list. It is the general
|
|
170
|
+
* shape "a hand-written matcher normalises differently from the router", and a
|
|
171
|
+
* migrated predicate still runs one string comparison for any SUB-PATH rule --
|
|
172
|
+
* in the shipped sample, the `/archived` deny. That comparison folds case but
|
|
173
|
+
* does not decode, so `GET /owners/%61rchived` steps past it. See the
|
|
174
|
+
* normalisation paragraph below and abofs/stonyx-orm#228.
|
|
175
|
+
*
|
|
176
|
+
* ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
|
|
177
|
+
* mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
|
|
178
|
+
* beneath the mount. The context names which model and which verb, NOT which
|
|
179
|
+
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
180
|
+
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
181
|
+
*
|
|
182
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
183
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
184
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
185
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
186
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
187
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
188
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
189
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
157
190
|
*
|
|
158
191
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
159
192
|
* string, which matches no collection, which falls through to the permission
|
|
160
|
-
* array -- a total grant. An input you cannot identify must DENY
|
|
193
|
+
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
194
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
195
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
196
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
197
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
161
198
|
*
|
|
162
|
-
* THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
|
|
163
|
-
* transport artifact being asked to stand in for a structural fact.
|
|
164
199
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
165
200
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|
|
166
201
|
* until #202 lands; the function shape is what requires any matching at all.
|
package/dist/orm-request.js
CHANGED
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
*
|
|
99
99
|
* PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
|
|
100
100
|
* the answer model-correct on its own -- the resolved predicate has to READ it.
|
|
101
|
-
* Measured against
|
|
102
|
-
*
|
|
101
|
+
* Measured against an ARITY-1 predicate, on a request express dispatched to
|
|
102
|
+
* `GET /owners/angela`, asked about ANIMALS:
|
|
103
103
|
*
|
|
104
104
|
* getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
105
105
|
* -> record => record.id !== 'angela' && record.id !== 'restricted'
|
|
@@ -110,23 +110,30 @@
|
|
|
110
110
|
* `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
|
|
111
111
|
* context was supplied and the answer is not the animal answer, and it is wrong
|
|
112
112
|
* in the GRANTING direction, because that predicate is arity-1 and identifies
|
|
113
|
-
* its collection from the request. (
|
|
114
|
-
*
|
|
113
|
+
* its collection from the request. (Asserted on a live dispatch by AC9 in
|
|
114
|
+
* test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
|
|
115
115
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* the
|
|
116
|
+
* This repo's own sample access class has since been MIGRATED to read the
|
|
117
|
+
* context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
|
|
118
|
+
* with the animal filter. That is not true of a consumer tree: an arity-1
|
|
119
|
+
* predicate keeps working -- the second argument is additive -- and the caller
|
|
120
|
+
* has no supported way to tell which kind it got. The boot-time arity warning
|
|
121
|
+
* that surfaces one is abofs/stonyx-orm#221.
|
|
119
122
|
* So: pass the context, and do not treat a resolved predicate's answer as
|
|
120
123
|
* model-specific until that predicate has been migrated to read the context.
|
|
121
124
|
*
|
|
122
125
|
* ---------------------------------------------------------------------------
|
|
123
126
|
* DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
|
|
124
127
|
* ---------------------------------------------------------------------------
|
|
125
|
-
* `auth()` below hands your
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
|
|
129
|
+
* argument two, and `context.model` already names the collection -- see the
|
|
130
|
+
* contract section above. Argument ONE is still the raw transport artifact, and
|
|
131
|
+
* everything from here to the end of this banner is the record of what happened
|
|
132
|
+
* when predicates worked the collection out from it. IT IS HISTORY, NOT
|
|
133
|
+
* GUIDANCE: do not write any of it into a new predicate. Every attempt to
|
|
134
|
+
* identify the collection by parsing the request target has failed OPEN. Five
|
|
135
|
+
* distinct variants of the same three-line example have now been found, each
|
|
136
|
+
* after the previous was fixed, by five different people:
|
|
130
137
|
*
|
|
131
138
|
* 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
|
|
132
139
|
* prefix match against it is ALWAYS false.
|
|
@@ -145,22 +152,50 @@
|
|
|
145
152
|
* last, and the record comes back in full. It walks past a hard
|
|
146
153
|
* `return false` deny the same way.
|
|
147
154
|
*
|
|
148
|
-
* The fix is not a sixth rule. It is to
|
|
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`. 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.
|
|
149
160
|
*
|
|
150
|
-
* `request.baseUrl`
|
|
151
|
-
*
|
|
152
|
-
* unaffected by absolute-form,
|
|
153
|
-
* `ORM_REST_ROUTE` prefix
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
161
|
+
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
162
|
+
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
163
|
+
* not mount-relative, unaffected by absolute-form, already carrying the
|
|
164
|
+
* configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
|
|
165
|
+
* standing in for a structural fact and the sample no longer does it.
|
|
166
|
+
* `context.model` IS the structural fact, so variants 1, 2, 4 and 5 are
|
|
167
|
+
* unconstructible against a migrated predicate rather than handled.
|
|
168
|
+
*
|
|
169
|
+
* VARIANT 3 SURVIVES, and is deliberately not in that list. It is the general
|
|
170
|
+
* shape "a hand-written matcher normalises differently from the router", and a
|
|
171
|
+
* migrated predicate still runs one string comparison for any SUB-PATH rule --
|
|
172
|
+
* in the shipped sample, the `/archived` deny. That comparison folds case but
|
|
173
|
+
* does not decode, so `GET /owners/%61rchived` steps past it. See the
|
|
174
|
+
* normalisation paragraph below and abofs/stonyx-orm#228.
|
|
175
|
+
*
|
|
176
|
+
* ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
|
|
177
|
+
* mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
|
|
178
|
+
* beneath the mount. The context names which model and which verb, NOT which
|
|
179
|
+
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
180
|
+
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
181
|
+
*
|
|
182
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
183
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
184
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
185
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
186
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
187
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
188
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
189
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
157
190
|
*
|
|
158
191
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
159
192
|
* string, which matches no collection, which falls through to the permission
|
|
160
|
-
* array -- a total grant. An input you cannot identify must DENY
|
|
193
|
+
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
194
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
195
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
196
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
197
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
161
198
|
*
|
|
162
|
-
* THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
|
|
163
|
-
* transport artifact being asked to stand in for a structural fact.
|
|
164
199
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
165
200
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|
|
166
201
|
* until #202 lands; the function shape is what requires any matching at all.
|
package/package.json
CHANGED
package/src/orm-request.ts
CHANGED
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
*
|
|
99
99
|
* PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
|
|
100
100
|
* the answer model-correct on its own -- the resolved predicate has to READ it.
|
|
101
|
-
* Measured against
|
|
102
|
-
*
|
|
101
|
+
* Measured against an ARITY-1 predicate, on a request express dispatched to
|
|
102
|
+
* `GET /owners/angela`, asked about ANIMALS:
|
|
103
103
|
*
|
|
104
104
|
* getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
105
105
|
* -> record => record.id !== 'angela' && record.id !== 'restricted'
|
|
@@ -110,23 +110,30 @@
|
|
|
110
110
|
* `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
|
|
111
111
|
* context was supplied and the answer is not the animal answer, and it is wrong
|
|
112
112
|
* in the GRANTING direction, because that predicate is arity-1 and identifies
|
|
113
|
-
* its collection from the request. (
|
|
114
|
-
*
|
|
113
|
+
* its collection from the request. (Asserted on a live dispatch by AC9 in
|
|
114
|
+
* test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
|
|
115
115
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* the
|
|
116
|
+
* This repo's own sample access class has since been MIGRATED to read the
|
|
117
|
+
* context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
|
|
118
|
+
* with the animal filter. That is not true of a consumer tree: an arity-1
|
|
119
|
+
* predicate keeps working -- the second argument is additive -- and the caller
|
|
120
|
+
* has no supported way to tell which kind it got. The boot-time arity warning
|
|
121
|
+
* that surfaces one is abofs/stonyx-orm#221.
|
|
119
122
|
* So: pass the context, and do not treat a resolved predicate's answer as
|
|
120
123
|
* model-specific until that predicate has been migrated to read the context.
|
|
121
124
|
*
|
|
122
125
|
* ---------------------------------------------------------------------------
|
|
123
126
|
* DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
|
|
124
127
|
* ---------------------------------------------------------------------------
|
|
125
|
-
* `auth()` below hands your
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
|
|
129
|
+
* argument two, and `context.model` already names the collection -- see the
|
|
130
|
+
* contract section above. Argument ONE is still the raw transport artifact, and
|
|
131
|
+
* everything from here to the end of this banner is the record of what happened
|
|
132
|
+
* when predicates worked the collection out from it. IT IS HISTORY, NOT
|
|
133
|
+
* GUIDANCE: do not write any of it into a new predicate. Every attempt to
|
|
134
|
+
* identify the collection by parsing the request target has failed OPEN. Five
|
|
135
|
+
* distinct variants of the same three-line example have now been found, each
|
|
136
|
+
* after the previous was fixed, by five different people:
|
|
130
137
|
*
|
|
131
138
|
* 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
|
|
132
139
|
* prefix match against it is ALWAYS false.
|
|
@@ -145,22 +152,50 @@
|
|
|
145
152
|
* last, and the record comes back in full. It walks past a hard
|
|
146
153
|
* `return false` deny the same way.
|
|
147
154
|
*
|
|
148
|
-
* The fix is not a sixth rule. It is to
|
|
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`. 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.
|
|
149
160
|
*
|
|
150
|
-
* `request.baseUrl`
|
|
151
|
-
*
|
|
152
|
-
* unaffected by absolute-form,
|
|
153
|
-
* `ORM_REST_ROUTE` prefix
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
161
|
+
* An intermediate revision of the sample read `request.baseUrl` -- the mount
|
|
162
|
+
* Express ACTUALLY MATCHED. That closed all five variants (no query string,
|
|
163
|
+
* not mount-relative, unaffected by absolute-form, already carrying the
|
|
164
|
+
* configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
|
|
165
|
+
* standing in for a structural fact and the sample no longer does it.
|
|
166
|
+
* `context.model` IS the structural fact, so variants 1, 2, 4 and 5 are
|
|
167
|
+
* unconstructible against a migrated predicate rather than handled.
|
|
168
|
+
*
|
|
169
|
+
* VARIANT 3 SURVIVES, and is deliberately not in that list. It is the general
|
|
170
|
+
* shape "a hand-written matcher normalises differently from the router", and a
|
|
171
|
+
* migrated predicate still runs one string comparison for any SUB-PATH rule --
|
|
172
|
+
* in the shipped sample, the `/archived` deny. That comparison folds case but
|
|
173
|
+
* does not decode, so `GET /owners/%61rchived` steps past it. See the
|
|
174
|
+
* normalisation paragraph below and abofs/stonyx-orm#228.
|
|
175
|
+
*
|
|
176
|
+
* ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
|
|
177
|
+
* mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
|
|
178
|
+
* beneath the mount. The context names which model and which verb, NOT which
|
|
179
|
+
* route, so the sample's `/archived` deny cannot be expressed from the context
|
|
180
|
+
* alone and a context-ONLY rewrite would silently turn that deny into an allow.
|
|
181
|
+
*
|
|
182
|
+
* NORMALISE THE WAY THE ROUTER DOES, AND CASE-FOLDING ALONE IS NOT THAT. The
|
|
183
|
+
* sample lower-cases before comparing, because a matcher stricter than the
|
|
184
|
+
* case-insensitive router can be stepped around. That closes the case gap only.
|
|
185
|
+
* Express sets `request.path` from the RAW, UNDECODED pathname while the router
|
|
186
|
+
* DECODES `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
187
|
+
* comparison as `/%61rchived` and walks past the deny. That gap is live in the
|
|
188
|
+
* sample and is tracked as abofs/stonyx-orm#228; the `.toLowerCase()` is not a
|
|
189
|
+
* complete normalisation recipe. Compare record ids at their real case.
|
|
157
190
|
*
|
|
158
191
|
* `?? ''` is not a defence. It converts an absent request target into an empty
|
|
159
192
|
* string, which matches no collection, which falls through to the permission
|
|
160
|
-
* array -- a total grant. An input you cannot identify must DENY
|
|
193
|
+
* array -- a total grant. An input you cannot identify must DENY, and that
|
|
194
|
+
* applies to BOTH arguments: since #202 the guard and the read can sit on
|
|
195
|
+
* different objects, and a guard on argument two does not protect a read of
|
|
196
|
+
* argument one. The sample returns `false` for an absent `model` AND for an
|
|
197
|
+
* absent or non-string `request.path`, rather than falling through either way.
|
|
161
198
|
*
|
|
162
|
-
* THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
|
|
163
|
-
* transport artifact being asked to stand in for a structural fact.
|
|
164
199
|
* THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
|
|
165
200
|
* the operation and the record. Prefer the array shape (`['read']`) or `false`
|
|
166
201
|
* until #202 lands; the function shape is what requires any matching at all.
|