@stonyx/orm 0.3.2-beta.227 → 0.3.2-beta.229
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 +84 -3
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -318,16 +318,97 @@ await setupRestServer('/', './access');
|
|
|
318
318
|
Access classes define models and provide custom filtering/authorization logic:
|
|
319
319
|
|
|
320
320
|
```js
|
|
321
|
-
export default class
|
|
322
|
-
models = ['owner'
|
|
321
|
+
export default class OwnerAccess {
|
|
322
|
+
models = ['owner'];
|
|
323
323
|
|
|
324
324
|
access(request) {
|
|
325
|
-
|
|
325
|
+
// `access` runs after route matching, so `request.params` is populated and
|
|
326
|
+
// `id` has already been URL-decoded. Authorize on it, never on a URL.
|
|
327
|
+
const { id } = request.params;
|
|
328
|
+
|
|
329
|
+
// `id` is still raw client text. Normalise it the way the record lookup
|
|
330
|
+
// does, or your predicate and the lookup disagree — see "Numeric ids" below.
|
|
331
|
+
// No radix on parseInt: that is deliberate, and it must stay that way.
|
|
332
|
+
const recordId = id === undefined ? undefined : (isNaN(id) ? id : parseInt(id));
|
|
333
|
+
|
|
334
|
+
// Returning false explicitly denies access to this record
|
|
335
|
+
if (recordId === 'angela') return false;
|
|
336
|
+
|
|
337
|
+
// No `id` means the collection route. Returning a function plugs it in to
|
|
338
|
+
// the response object as a filter. NOTE: a function return authorizes the
|
|
339
|
+
// request outright — the operations list below is not consulted — so this
|
|
340
|
+
// branch permits POST /owners as well as reads.
|
|
341
|
+
if (recordId === undefined) return record => record.id !== 'angela';
|
|
342
|
+
|
|
343
|
+
// Returning a list of operations allows full access to everything else
|
|
344
|
+
return ['read', 'create', 'update', 'delete'];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Do not authorize on the request URL.** `request.url` is rewritten relative to
|
|
350
|
+
the mount point, so inside the REST server it is `/angela`, not `/owners/angela`
|
|
351
|
+
— a suffix comparison against it never matches and the request falls through to
|
|
352
|
+
whatever the method returns next. `request.originalUrl` keeps the full path but
|
|
353
|
+
is still the raw text the client sent, so it varies with query strings
|
|
354
|
+
(`/owners?x=1`), trailing slashes (`/owners/angela/`), casing (`/OwNeRs/angela`)
|
|
355
|
+
and percent-encoding (`/owners/%61ngela`). Each of those is a plain address-bar
|
|
356
|
+
request. Which of them reach your handler at all depends on how the REST server
|
|
357
|
+
configures Express route matching — that is a deployment detail you should not
|
|
358
|
+
be building an authorization decision on top of. `request.params.id` is
|
|
359
|
+
identical for every spelling that does reach you.
|
|
360
|
+
|
|
361
|
+
**One access class per model when the rules are model-specific.** `access()`
|
|
362
|
+
receives only the request, and the request does not carry the model name
|
|
363
|
+
directly — `request.baseUrl` is the *mount text as the client spelled it*
|
|
364
|
+
(`/OWNERS`), so it must be case-normalised before it is compared, and it is
|
|
365
|
+
still the mount rather than the model. Deriving a model from it means every
|
|
366
|
+
unrecognised spelling falls through to whatever your method returns next, so
|
|
367
|
+
prefer one class per model. A class may still list several models in `models`
|
|
368
|
+
when they share one rule.
|
|
369
|
+
|
|
370
|
+
**Numeric ids: normalise before you compare.** `request.params.id` is raw text
|
|
371
|
+
from the client. When it looks numeric the ORM coerces it — `isNaN(id) ? id :
|
|
372
|
+
parseInt(id)` — *before* it resolves the record, so `7`, `007`, `7.0`, `7.9`,
|
|
373
|
+
`7e0`, `0x7`, `+7`, `%207` (a leading space), `%097` (a tab) and `7%0A` (a
|
|
374
|
+
trailing newline) all address record `7`, while a `===` against the raw text
|
|
375
|
+
matches only the one spelling you wrote down. Every other spelling falls through
|
|
376
|
+
to whatever your method returns next — which, in the shape above, is a full CRUD
|
|
377
|
+
grant. All of them are plain address-bar requests.
|
|
378
|
+
|
|
379
|
+
Two details are load-bearing. `parseInt` is called with **no radix**, so `0x7`
|
|
380
|
+
is `7` and not `0`; writing `parseInt(id, 10)` in your predicate re-opens the
|
|
381
|
+
hex spelling. And the coercion applies only when the id looks numeric, so a
|
|
382
|
+
model with string ids (like `owner` above) is unaffected — which is exactly why
|
|
383
|
+
this is easy to miss. Normalise the same way the lookup does:
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
export default class AnimalAccess {
|
|
387
|
+
models = ['animal'];
|
|
388
|
+
|
|
389
|
+
access(request) {
|
|
390
|
+
const { id } = request.params;
|
|
391
|
+
|
|
392
|
+
// Agrees with the lookup for every spelling of 7 above. Compare the
|
|
393
|
+
// coerced value, which for a numeric-id model is a number, not a string.
|
|
394
|
+
const recordId = id === undefined ? undefined : (isNaN(id) ? id : parseInt(id));
|
|
395
|
+
|
|
396
|
+
if (recordId === 7) return false;
|
|
397
|
+
|
|
398
|
+
if (recordId === undefined) return record => record.id !== 7;
|
|
399
|
+
|
|
326
400
|
return ['read', 'create', 'update', 'delete'];
|
|
327
401
|
}
|
|
328
402
|
}
|
|
329
403
|
```
|
|
330
404
|
|
|
405
|
+
Both samples above are extracted from this file and executed verbatim against a
|
|
406
|
+
live server on every CI run — see
|
|
407
|
+
[`test/integration/readme-access/`](https://github.com/abofs/stonyx-orm/tree/dev/test/integration/readme-access).
|
|
408
|
+
`DELETE /owners/angela` is asserted to be refused with the record intact, and
|
|
409
|
+
every spelling named above is measured individually, the numeric ones over a raw
|
|
410
|
+
socket. Nothing in this section is prose that was never run.
|
|
411
|
+
|
|
331
412
|
### Upgrading: behaviour changes
|
|
332
413
|
|
|
333
414
|
**Advertised `links.self` / `links.related` now carry the REST mount route.**
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.2-beta.
|
|
7
|
+
"version": "0.3.2-beta.229",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"type": "module",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stonyx/cron": "0.2.1-beta.
|
|
64
|
+
"@stonyx/cron": "0.2.1-beta.122",
|
|
65
65
|
"@stonyx/events": "0.1.1-beta.64",
|
|
66
66
|
"@stonyx/utils": "0.2.3-beta.27",
|
|
67
67
|
"stonyx": "0.2.3-beta.94"
|
|
@@ -103,8 +103,10 @@
|
|
|
103
103
|
"scripts": {
|
|
104
104
|
"build": "tsc",
|
|
105
105
|
"build:test": "tsc -p tsconfig.test.json",
|
|
106
|
-
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts' && ORM_TEST_ROUTE=/ pnpm test:mounted && ORM_TEST_ROUTE=/api pnpm test:mounted && ORM_TEST_ROUTE=api pnpm test:mounted && ORM_TEST_ROUTE=/api/v1 pnpm test:mounted && ORM_TEST_ROUTE=/api/ pnpm test:mounted",
|
|
106
|
+
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts' && ORM_TEST_ROUTE=/ pnpm test:mounted && ORM_TEST_ROUTE=/api pnpm test:mounted && ORM_TEST_ROUTE=api pnpm test:mounted && ORM_TEST_ROUTE=/api/v1 pnpm test:mounted && ORM_TEST_ROUTE=/api/ pnpm test:mounted && pnpm test:readme && pnpm test:reference",
|
|
107
107
|
"test:mounted": "node --import tsx/esm --import ./test/integration/mounted-route/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/mounted-route/links-mounted.ts' 'test/zz-exit-test.ts'",
|
|
108
|
+
"test:readme": "pnpm build && node --import tsx/esm --import ./test/integration/readme-access/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/readme-access/readme-sample.ts' 'test/zz-exit-test.ts'",
|
|
109
|
+
"test:reference": "pnpm build && node --import tsx/esm --import ./test/integration/reference-access/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/reference-access/reference-sample.ts' 'test/zz-exit-test.ts'",
|
|
108
110
|
"test:dynamodb": "pnpm build && node --import tsx/esm --import ./test/integration/dynamodb/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/dynamodb/**/*-test.ts'"
|
|
109
111
|
}
|
|
110
112
|
}
|