@hitchy/plugin-odem-rest 0.5.6 → 0.6.0

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.
@@ -28,7 +28,7 @@ module.exports = function() {
28
28
  /**
29
29
  * Handles CORS-related behaviours.
30
30
  *
31
- * @name api.runtime.services.OdemRestCors
31
+ * @name api.services.OdemRestCors
32
32
  */
33
33
  class OdemRestCors {
34
34
  /**
@@ -84,7 +84,7 @@ module.exports = function() {
84
84
  static getRequestFilterForModel( model ) { // eslint-disable-line no-unused-vars
85
85
  return ( req, res, next ) => {
86
86
  if ( !res.headersSent ) {
87
- if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
87
+ if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
88
88
  this.handleMethods( model, null, req, res, Accepted.model.methods );
89
89
 
90
90
  if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
@@ -112,7 +112,7 @@ module.exports = function() {
112
112
  static getRequestFilterForModelSchema( model ) { // eslint-disable-line no-unused-vars
113
113
  return ( req, res, next ) => {
114
114
  if ( !res.headersSent ) {
115
- if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
115
+ if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
116
116
  this.handleMethods( model, null, req, res, Accepted.schema.methods );
117
117
 
118
118
  if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
@@ -140,7 +140,7 @@ module.exports = function() {
140
140
  static getRequestFilterForModelItem( model ) { // eslint-disable-line no-unused-vars
141
141
  return ( req, res, next ) => {
142
142
  if ( !res.headersSent && req.params.uuid !== ".schema" ) {
143
- if ( Services.OdemRestSchema.mayBeExposed( req, model ) ) {
143
+ if ( Services.OdemSchema.mayBeExposed( req, model ) ) {
144
144
  this.handleMethods( model, req.params.uuid, req, res, Accepted.item.methods );
145
145
 
146
146
  if ( res.hasHeader( "Access-Control-Allow-Origin" ) ) {
package/index.js CHANGED
@@ -23,6 +23,10 @@ module.exports = function() {
23
23
  before.set( `ALL ${resolve( urlPrefix, ".schema" )}`, CORS.getRequestFilterForSchemata() );
24
24
  after.set( `ALL ${resolve( urlPrefix, ".schema" )}`, reqNotSupported );
25
25
 
26
+ if ( api.plugins.authentication ) {
27
+ before.set( `GET ${resolve( urlPrefix, ".schema" )}`, Services.AuthorizationPolicyGenerator.mayAccess( "@hitchy.odem.schema" ) );
28
+ }
29
+
26
30
  for ( let i = 0, numNames = modelNames.length; i < numNames; i++ ) {
27
31
  const name = modelNames[i];
28
32
  const routeName = Case.pascalToKebab( name );
@@ -32,6 +36,55 @@ module.exports = function() {
32
36
  before.set( `ALL ${resolve( urlPrefix, routeName, ".schema" )}`, CORS.getRequestFilterForModelSchema( model ) );
33
37
  before.set( `ALL ${resolve( urlPrefix, routeName, ":uuid" )}`, CORS.getRequestFilterForModelItem( model ) );
34
38
 
39
+ if ( api.plugins.authentication ) {
40
+ // add policies for additionally checking a requesting user's authorization
41
+ const modelUrl = resolve( urlPrefix, routeName );
42
+ const policy = action => Services.AuthorizationPolicyGenerator.mayAccess( `@hitchy.odem.model.${name}.${action}` );
43
+
44
+ const handlers = {
45
+ schema: policy( "schema" ),
46
+ create: policy( "create" ),
47
+ list: policy( "list" ),
48
+ read: policy( "read" ),
49
+ write: policy( "write" ),
50
+ check: policy( "check" ),
51
+ remove: policy( "remove" ),
52
+ };
53
+
54
+ // convenience routes
55
+ before.set( `GET ${resolve( modelUrl, "create" )}`, handlers.create );
56
+ before.set( `GET ${resolve( modelUrl, "write", ":uuid" )}`, handlers.write );
57
+ before.set( `GET ${resolve( modelUrl, "replace", ":uuid" )}`, handlers.write );
58
+ before.set( `GET ${resolve( modelUrl, "has", ":uuid" )}`, handlers.check );
59
+ before.set( `GET ${resolve( modelUrl, "remove", ":uuid" )}`, handlers.remove );
60
+
61
+ // extended routes
62
+ before.set( `GET ${resolve( modelUrl, ".schema" )}`, policy( "schema" ) );
63
+
64
+ // REST-compliant routes
65
+ before.set( `GET ${modelUrl}`, function( req, res, next ) {
66
+ const tail = req.url.substring( modelUrl.length ).trim();
67
+
68
+ if ( tail && tail !== "/" ) {
69
+ next();
70
+ } else {
71
+ handlers.list.call( this, req, res, next );
72
+ }
73
+ } );
74
+ before.set( `POST ${resolve( modelUrl )}`, handlers.create );
75
+ before.set( `GET ${resolve( modelUrl, ":uuid" )}`, ( req, res, next ) => {
76
+ if ( req.params.uuid === ".schema" ) {
77
+ next();
78
+ } else {
79
+ handlers.read.call( this, req, res, next );
80
+ }
81
+ } );
82
+ before.set( `PATCH ${resolve( modelUrl, ":uuid" )}`, handlers.write );
83
+ before.set( `PUT ${resolve( modelUrl, ":uuid" )}`, handlers.write );
84
+ before.set( `HEAD ${resolve( modelUrl, ":uuid" )}`, handlers.check );
85
+ before.set( `DELETE ${resolve( modelUrl, ":uuid" )}`, handlers.remove );
86
+ }
87
+
35
88
  after.set( `ALL ${resolve( urlPrefix, routeName )}`, reqNotSupported );
36
89
  }
37
90
 
@@ -90,7 +143,7 @@ module.exports = function() {
90
143
  * @returns {void}
91
144
  */
92
145
  function addGlobalRoutes( routes, urlPrefix, models ) {
93
- const { runtime: { services: { Model: BaseModel, OdemRestSchema } }, utility: { case: { pascalToKebab } } } = api;
146
+ const { runtime: { services: { Model: BaseModel, OdemSchema } }, utility: { case: { pascalToKebab } } } = api;
94
147
 
95
148
  routes.set( `GET ${resolve( urlPrefix, ".schema" )}`, reqFetchSchemata );
96
149
 
@@ -111,11 +164,11 @@ module.exports = function() {
111
164
  const model = models[modelKeys[i]];
112
165
 
113
166
  if ( model.prototype instanceof BaseModel &&
114
- OdemRestSchema.mayBeExposed( req, model ) &&
115
- OdemRestSchema.mayBePromoted( req, model ) ) {
167
+ OdemSchema.mayBeExposed( req, model ) &&
168
+ OdemSchema.mayBePromoted( req, model ) ) {
116
169
  const slug = pascalToKebab( model.name );
117
170
 
118
- result[slug] = OdemRestSchema.extractPublicData( model );
171
+ result[slug] = OdemSchema.extractPublicData( model );
119
172
  }
120
173
  }
121
174
 
@@ -135,7 +188,7 @@ module.exports = function() {
135
188
  * @returns {void}
136
189
  */
137
190
  function addRoutesOnModel( routes, urlPrefix, routeName, Model, includeConvenienceRoutes ) {
138
- const { Model: BaseModel, OdemRestSchema: Schema, OdemUtilityUuid: { ptnUuid } } = Services;
191
+ const { Model: BaseModel, OdemSchema: Schema, OdemUtilityUuid: { ptnUuid } } = Services;
139
192
 
140
193
  const modelUrl = resolve( urlPrefix, routeName );
141
194
 
@@ -182,7 +235,7 @@ module.exports = function() {
182
235
  * @returns {void}
183
236
  */
184
237
  function reqSuccess( req, res ) {
185
- if ( Services.OdemRestSchema.mayBeExposed( req, Model ) ) {
238
+ if ( Services.OdemSchema.mayBeExposed( req, Model ) ) {
186
239
  res.status( 200 ).send();
187
240
  } else {
188
241
  res.status( 403 ).send();
@@ -212,7 +265,7 @@ module.exports = function() {
212
265
  function reqFetchSchema( req, res ) {
213
266
  logDebug( "got request fetching schema" );
214
267
 
215
- if ( !Services.OdemRestSchema.mayBeExposed( req, Model ) ) {
268
+ if ( !Services.OdemSchema.mayBeExposed( req, Model ) ) {
216
269
  res.status( 403 ).json( { error: "access forbidden by model" } );
217
270
  return;
218
271
  }
@@ -278,7 +331,7 @@ module.exports = function() {
278
331
  const item = new Model( uuid ); // eslint-disable-line new-cap
279
332
 
280
333
  return item.load()
281
- .then( loaded => res.json( loaded.toObject( { serialized: true } ) ) )
334
+ .then( loaded => res.json( Schema.filterItem( loaded.toObject( { serialized: true } ), req, Model, "read" ) ) )
282
335
  .catch( error => {
283
336
  logError( "fetching %s:", routeName, error );
284
337
 
@@ -332,7 +385,6 @@ module.exports = function() {
332
385
  */
333
386
  function parseQuery( query ) {
334
387
  const simpleTernary = /^([^:\s]+):between:([^:]+):([^:]+)$/i.exec( query );
335
-
336
388
  if ( simpleTernary ) {
337
389
  const [ , name, lower, upper ] = simpleTernary;
338
390
 
@@ -422,13 +474,13 @@ module.exports = function() {
422
474
 
423
475
  const meta = count || req.headers["x-count"] ? {} : null;
424
476
 
425
- await Model.find( parsedQuery, { offset, limit, sortBy, sortAscendingly: !descending }, {
477
+ await Model.find( Schema.checkQuery( parsedQuery, req, Model ), { offset, limit, sortBy, sortAscendingly: !descending }, {
426
478
  metaCollector: meta,
427
479
  loadRecords
428
480
  } )
429
481
  .then( matches => {
430
482
  const result = {
431
- items: matches.map( m => m.toObject( { serialized: true } ) ),
483
+ items: matches.map( item => Schema.filterItem( item.toObject( { serialized: true } ), req, Model, "list" ) ),
432
484
  };
433
485
 
434
486
  if ( meta ) {
@@ -471,7 +523,7 @@ module.exports = function() {
471
523
  }, { loadRecords, metaCollector: meta } )
472
524
  .then( matches => {
473
525
  const result = {
474
- items: matches.map( m => m.toObject( { serialized: true } ) ),
526
+ items: matches.map( item => Schema.filterItem( item.toObject( { serialized: true } ), req, Model, "list" ) ),
475
527
  };
476
528
 
477
529
  if ( meta ) {
@@ -507,26 +559,20 @@ module.exports = function() {
507
559
 
508
560
  return ( req.method === "GET" ? Promise.resolve( req.query ) : req.fetchBody() )
509
561
  .then( record => {
510
- if ( record.uuid ) {
511
- logDebug( "creating %s:", routeName, "new entry can not be created with uuid" );
512
- res.status( 400 ).json( { error: "new entry can not be created with uuid" } );
513
- return undefined;
514
- }
515
-
516
562
  if ( record ) {
517
- const names = Object.keys( record );
518
- const numNames = names.length;
563
+ if ( record.uuid ) {
564
+ logDebug( "creating %s:", routeName, "new entry can not be created with uuid" );
565
+ res.status( 400 ).json( { error: "new entry can not be created with uuid" } );
566
+ return undefined;
567
+ }
519
568
 
569
+ const filtered = Schema.filterItem( record, req, Model, "create" );
520
570
  const definedProps = Model.schema.props;
521
571
  const definedComputed = Model.schema.computed;
522
572
 
523
- for ( let i = 0; i < numNames; i++ ) {
524
- const name = names[i];
525
-
526
- if ( definedProps[name] ) {
527
- item.$properties[name] = record[name];
528
- } else if ( definedComputed[name] ) {
529
- item[name] = record[name];
573
+ for ( const [ key, value ] of Object.entries( filtered ) ) {
574
+ if ( definedProps[key]?.$type || definedComputed[key] ) {
575
+ item[key] = value;
530
576
  }
531
577
  }
532
578
  }
@@ -578,27 +624,20 @@ module.exports = function() {
578
624
  ] )
579
625
  .then( ( [ loaded, record ] ) => {
580
626
  if ( record ) {
581
- // FIXME consider using Odem's Model.fromObject() method here
582
- const names = Object.keys( record );
583
- const numNames = names.length;
584
-
627
+ const filtered = Schema.filterItem( record, req, Model, "write" );
585
628
  const definedProps = Model.schema.props;
586
629
  const definedComputed = Model.schema.computed;
587
630
 
588
- for ( let i = 0; i < numNames; i++ ) {
589
- const name = names[i];
590
-
591
- if ( definedProps[name] ) {
592
- loaded.$properties[name] = record[name]; // eslint-disable-line no-param-reassign
593
- } else if ( definedComputed[name] ) {
594
- loaded[name] = record[name]; // eslint-disable-line no-param-reassign
631
+ for ( const [ key, value ] of Object.entries( filtered ) ) {
632
+ if ( definedProps[key]?.$type || definedComputed[key] ) {
633
+ loaded[key] = value; // eslint-disable-line no-param-reassign
595
634
  }
596
635
  }
597
636
  }
598
637
 
599
638
  return loaded.save()
600
639
  .then( saved => {
601
- res.json( saved.toObject( { serialized: true } ) );
640
+ res.json( Schema.filterItem( saved.toObject( { serialized: true } ), req, Model, "read" ) );
602
641
  } );
603
642
  } );
604
643
  } )
@@ -640,30 +679,27 @@ module.exports = function() {
640
679
 
641
680
  const computedNames = Object.keys( Model.schema.computed );
642
681
  const numComputedNames = computedNames.length;
682
+ const filtered = Schema.filterItem( record, req, Model, "write" );
643
683
 
644
684
  // update properties, drop those missing in provided record
645
685
  for ( let i = 0; i < numPropNames; i++ ) {
646
686
  const propName = propNames[i];
647
- const value = record[propName];
687
+ const value = filtered[propName];
648
688
 
649
- if ( value == null ) {
650
- item.$properties[propName] = null;
651
- } else {
652
- item.$properties[propName] = value;
653
- }
689
+ item[propName] = value ?? null;
654
690
  }
655
691
 
656
692
  // assign all posted computed properties
657
693
  for ( let i = 0; i < numComputedNames; i++ ) {
658
694
  const computedName = computedNames[i];
659
- item[computedName] = record[computedName];
695
+ item[computedName] = filtered[computedName];
660
696
  }
661
697
 
662
698
  return item.save( { ignoreUnloaded: !exists } );
663
699
  } );
664
700
  } )
665
701
  .then( saved => {
666
- res.json( { uuid: saved.uuid } );
702
+ res.json( Schema.filterItem( saved.toObject( { serialized: true } ), req, Model, "read" ) );
667
703
  } )
668
704
  .catch( error => {
669
705
  logError( "updating %s:", routeName, error );
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@hitchy/plugin-odem-rest",
3
- "version": "0.5.6",
3
+ "version": "0.6.0",
4
4
  "description": "HTTP REST API for Hitchy's ODM",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "lint": "eslint .",
8
- "test": "hitchy-pm odem --exec mocha --ui=tdd 'test/scripts/**/*.js'",
9
- "coverage": "hitchy-pm odem --exec c8 mocha --ui=tdd 'test/scripts/**/*.js'"
8
+ "test": "hitchy-pm odem auth --exec c8 mocha --ui=tdd 'test/scripts/**/*.js'"
10
9
  },
11
10
  "repository": "https://gitlab.com/hitchy/plugin-odem-rest.git",
12
11
  "keywords": [
@@ -18,17 +17,18 @@
18
17
  "bugs": "https://gitlab.com/hitchy/plugin-odem-rest/-/issues",
19
18
  "homepage": "https://gitlab.com/hitchy/plugin-odem-rest#plugin-odem-rest",
20
19
  "peerDependencies": {
21
- "@hitchy/core": "^0.8.x",
22
- "@hitchy/plugin-odem": "^0.7.8"
20
+ "@hitchy/core": "0.8.x",
21
+ "@hitchy/plugin-odem": "0.8.x",
22
+ "@hitchy/plugin-auth": "0.4.x"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@hitchy/types": "^0.1.3",
26
- "@hitchy/server-dev-tools": "^0.4.8",
27
- "c8": "^8.0.1",
28
- "eslint": "^8.47.0",
29
- "eslint-config-cepharum": "^1.0.13",
26
+ "@hitchy/server-dev-tools": "^0.4.9",
27
+ "c8": "^10.1.2",
28
+ "eslint": "^8.57.0",
29
+ "eslint-config-cepharum": "^1.0.14",
30
30
  "eslint-plugin-promise": "^6.1.1",
31
- "mocha": "^10.2.0",
31
+ "mocha": "^10.7.0",
32
32
  "should": "^13.2.3",
33
33
  "should-http": "^0.1.1"
34
34
  }
package/readme.md CHANGED
@@ -21,15 +21,34 @@ npm i @hitchy/plugin-odem-rest @hitchy/plugin-odem
21
21
  The command is installing this plugin and the additionally required [@hitchy/plugin-odem](https://www.npmjs.com/package/@hitchy/plugin-odem).
22
22
 
23
23
  :::warning Compatibility
24
- Starting with version 0.4.0 the latter plugin must be installed
25
- explicitly.
26
- :::
24
+ Starting with version 0.4.0 the latter plugin must be installed explicitly.
25
+ :::
26
+
27
+ :::warning Compatibility
28
+ Starting with version 0.6.0 authorization is tightened as soon as [@hitchy/plugin-auth](https://auth.hitchy.org) is discovered in current project. You need an [additional configuration](#authorization) to keep the REST API as open as before.
29
+ :::
27
30
 
28
31
  ## Usage
29
32
 
30
33
  This package depends on [@hitchy/plugin-odem](https://odem.hitchy.org/) and its preparation of model definitions discovered by [Hitchy's core](https://core.hitchy.org/). See the linked manuals for additional information.
31
34
 
32
- For a quick start example create a file **api/models/local-employee.js** in your Hitchy-based application with the following content:
35
+ In addition, [@hitchy/plugin-auth](https://auth.hitchy.org) is supported resulting in a tightened access control. When using that plugin in a project, a file **config/auth.js** with following content is necessary to test-drive this plugin's REST API:
36
+
37
+ ```javascript
38
+ exports.auth = {
39
+ authorizations: {
40
+ "@hitchy.odem": "*"
41
+ }
42
+ };
43
+ ```
44
+
45
+ > **Do not use this in a production setup!**
46
+ >
47
+ > This example is granting permissions to create, adjust and remove items of your ODM without any authentication. In addition, all _protected_ models and properties get exposed to everyone.
48
+ >
49
+ > See the [section on authorization](#authorization) for additional information!
50
+
51
+ Now, for a quick start example create a file **api/models/local-employee.js** in your Hitchy-based application with the following content:
33
52
 
34
53
  ```javascript
35
54
  module.exports = {
@@ -60,7 +79,7 @@ module.exports = {
60
79
  };
61
80
  ```
62
81
 
63
- When starting your Hitchy-based application it will discover a model named **LocalEmployee** and expose it via REST API using URLs like `/api/local-employee` just because of this package and its dependencies mentioned before.
82
+ When starting your Hitchy-based application it will discover a model named **LocalEmployee** and expose it via REST API using base URL `/api/local-employee` just because of this package and its dependencies mentioned before.
64
83
 
65
84
  #### Models of Hitchy plugins
66
85
 
@@ -69,7 +88,7 @@ Due to the way Hitchy is discovering plugins and compiling [components](https://
69
88
 
70
89
  ### How it works
71
90
 
72
- This plugin is defining a set of [blueprint routes](https://core.hitchy.org/internals/routing-basics.html#focusing-on-routes) implementing REST API for every model defined in file system as described before.
91
+ This plugin is defining a set of [blueprint routes](https://core.hitchy.org/internals/routing-basics.html#focusing-on-routes) implementing a REST API for every model defined in file system as described before.
73
92
 
74
93
  Those routes comply with this pattern:
75
94
 
@@ -231,3 +250,73 @@ Using query parameter `sortBy=lastName` a fetched list of items is sorted by val
231
250
  Query parameter `limit=n` is requesting to fetch at most **n** items. Parameter `offset=n` is requesting to skip **n** items before starting retrieval. Slicing is applied after sorting items.
232
251
 
233
252
  When slicing this way only a subset of basically available items is fetched by intention. If you need to know the total number of available items when requesting a slice you can either set custom field `x-count` in request header or query parameter `count` to `1` or any other truthy value. This will have a slight negative impact on request performance, but causes delivery of the total number of matching items in a separate property `count` of response body as well as in response header named `x-count`.
253
+
254
+ #### Authorization
255
+
256
+ As soon as [@hitchy/plugin-auth](https://auth.hitchy.org) is included with your project, **@hitchy/plugin-odem-rest** is using it to check a requesting user's authorization by declaring named [resources](https://auth.hitchy.org/introduction.html#resources) available for setting up authorization rules in [database](https://auth.hitchy.org/api/model/authorization-rule.html) or in [configuration](https://auth.hitchy.org/api/config.html#config-auth-authorizations).
257
+
258
+ ##### Resource namespace
259
+
260
+ All declared resources share common prefix `@hitchy.odem`.
261
+
262
+ Despite the generic name, the authorization control described here does not apply to **@hitchy/plugin-odem** in general. Custom server-side code needs to stay capable of interacting with models and their private or protected properties. Instead, authorization control affects the REST API implemented by this plugin, only. Other plugins may implement APIs for accessing the same models by different means. Those plugins may share the resource naming pattern described here so that all APIs featuring remote interaction with the data can be controlled in the same way. Thus, resource names omit to refer to the REST API explicitly.
263
+
264
+ ##### Per-model resources
265
+
266
+ Resources are declared to control authorizations for basically interacting with either model:
267
+
268
+ * When finding or listing items, accessing the resource `@hitchy.odem.model.<ModelName>.list` must be granted.
269
+ * When checking an item, accessing the resource `@hitchy.odem.model.<ModelName>.check` must be granted.
270
+ * When reading an item, accessing the resource `@hitchy.odem.model.<ModelName>.read` must be granted.
271
+ * When patching or replacing an item, accessing the resource `@hitchy.odem.model.<ModelName>.write` must be granted.
272
+ * When creating a new item, accessing the resource `@hitchy.odem.model.<ModelName>.create` must be granted.
273
+ * When removing an item, accessing the resource `@hitchy.odem.model.<ModelName>.remove` must be granted.
274
+ * Accessing a model's schema requires the resource `@hitchy.odem.model.<ModelName>.schema` to be granted.
275
+ * Accessing the collection of all models' schemata requires the resource `@hitchy.odem.schema` to be granted. This implicitly causes models with their [promote option](https://odem.hitchy.org/guides/defining-models.html#options) being `protected` to be exposed in responses to that request unless a more specific resource `@hitchy.odem.schema.model.<ModelName>` isn't revoked from the user.
276
+
277
+ > In these examples, replace `<ModelName>` with a model's name in PascalCase.
278
+
279
+ ##### Per-property resources
280
+
281
+ In addition, resources are declared to control access on a model's protected properties. By default, protected properties are hidden from a user's REST request.
282
+
283
+ > Prior to supporting **@hitchy/plugin-auth** for authoriation control in v0.6.0, protected properties have been available to any authenticated user. The same behavior still applies if **@hitchy/plugin-auth** isn't available in a project.
284
+
285
+ * When finding or listing items, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.list` must be granted for the protected property to be supported in filter queries and to include it in resulting set of items.
286
+ * When reading an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.read` must be granted to include the property in the resulting record.
287
+ * When creating an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.create` must be granted to include the property in the resulting record.
288
+ * When patching or replacing an item, accessing the resource `@hitchy.odem.model.<ModelName>.property.<propertyName>.write` must be granted to consider an update for the property and to include it in the resulting record.
289
+
290
+ > In these examples, replace `<ModelName>` with a model's name in PascalCase and `<propertyName>` with its name as defined in the model.
291
+
292
+ Per-property authorization control is not considered for public and private properties. This is meant to limit performance penalties on processing requests.
293
+
294
+ * Public properties are always available via REST API as soon as the according [per-model authorization](#per-model-resources) is given.
295
+ * Private properties are never available.
296
+
297
+ ##### Examples
298
+
299
+ The following example for a file **config/auth.js** is effectively disabling all authorization control in context of **@hitchy/plugin-odem-rest** and thus should be used for evaluation purposes, only:
300
+
301
+ ```javascript
302
+ exports.auth = {
303
+ authorizations: {
304
+ "@hitchy.odem": "*"
305
+ }
306
+ };
307
+ ```
308
+
309
+ A slightly more sophisticated example could look like this:
310
+
311
+ ```javascript
312
+ exports.auth = {
313
+ authorizations: {
314
+ "@hitchy.odem": "*",
315
+ "@hitchy.odem.model.User": [ "-*", "@admin" ],
316
+ "@hitchy.odem.model.Role": [ "-*", "@admin" ],
317
+ "@hitchy.odem.model.User.property.password": [ "-*" ],
318
+ }
319
+ };
320
+ ```
321
+
322
+ This example is granting access to all features of **@hitchy/plugin-odem-rest** unless they address either model `User` or `Role`. Access on those models is limited to users with role `admin`. However, even those users must not access protected property `password`, though you might want to declare it as private anyway in which case last rule in given example does not have any effect.
@@ -1,208 +0,0 @@
1
- "use strict";
2
-
3
- const PtnAcceptsValue = /^\s*(?:function\s*(?:\s\S+\s*)?)?\(\s*[^\s)]/;
4
-
5
- module.exports = function() {
6
- /**
7
- * Handles model schemata in context of REST-API plugin.
8
- *
9
- * @name api.runtime.services.OdemRestSchema
10
- */
11
- class OdemRestSchema {
12
- /**
13
- * Detects if selected model's schema may be promoted to clients or not.
14
- *
15
- * @param {HitchyIncomingMessage} request request descriptor
16
- * @param {class<Model>} model class of model to check
17
- * @returns {boolean} true if model's schema may be promoted to clients, false otherwise
18
- */
19
- static mayBePromoted( request, model ) {
20
- const { schema: { options } } = model;
21
-
22
- const scope = String( options.promote || options.expose || "public" ).trim().toLowerCase();
23
-
24
- switch ( scope ) {
25
- case "public" :
26
- return true;
27
-
28
- case "protected" :
29
- return Boolean( request.user );
30
-
31
- default :
32
- return false;
33
- }
34
- }
35
-
36
- /**
37
- * Detects if selected model may be exposed to clients or not.
38
- *
39
- * @param {HitchyIncomingMessage} request request descriptor
40
- * @param {class<Model>} model class of model to check
41
- * @returns {boolean} true if model may be exposed to clients, false otherwise
42
- */
43
- static mayBeExposed( request, model ) {
44
- const { schema: { options } } = model;
45
-
46
- const scope = String( options.expose || "public" ).trim().toLowerCase();
47
-
48
- switch ( scope ) {
49
- case "public" :
50
- return true;
51
-
52
- case "protected" :
53
- return Boolean( request.user );
54
-
55
- default :
56
- return false;
57
- }
58
- }
59
-
60
- /**
61
- * Extracts information on selected model's schema for publishing.
62
- *
63
- * @param {class<Model>} model model to process
64
- * @param {boolean} omitComputed set true to omit computed properties
65
- * @returns {object} extracted public information on selected model's schema
66
- */
67
- static extractPublicData( model, { omitComputed = false } = {} ) {
68
- const { schema: { props, computed, options } } = model;
69
-
70
- const extracted = {
71
- name: model.name,
72
- props: {},
73
- };
74
-
75
- const propNames = Object.keys( props );
76
- const numProps = propNames.length;
77
-
78
- for ( let i = 0; i < numProps; i++ ) {
79
- const name = propNames[i];
80
- const prop = props[name];
81
-
82
- const copy = extracted.props[name] = {
83
- type: prop.type || "string",
84
- };
85
-
86
- const optionNames = Object.keys( prop );
87
- const numOptions = optionNames.length;
88
-
89
- for ( let j = 0; j < numOptions; j++ ) {
90
- const optionName = optionNames[j];
91
-
92
- switch ( optionName.toLowerCase() ) {
93
- case "type" :
94
- case "indexes" :
95
- case "indices" :
96
- case "index" :
97
- break;
98
-
99
- default : {
100
- const option = prop[optionName];
101
-
102
- if ( option != null && typeof option !== "function" ) {
103
- copy[optionName] = this.cloneSerializable( option );
104
- }
105
- }
106
- }
107
- }
108
- }
109
-
110
- if ( !omitComputed ) {
111
- const computedNames = Object.keys( props );
112
- const numComputed = computedNames.length;
113
- extracted.computed = {};
114
-
115
- for ( let i = 0; i < numComputed; i++ ) {
116
- const name = computedNames[i];
117
- const fn = computed[name];
118
-
119
- if ( typeof fn === "function" ) {
120
- extracted.computed[name] = PtnAcceptsValue.test( fn );
121
- }
122
- }
123
- }
124
-
125
- const optionsNames = Object.keys( options );
126
- const numOptions = optionsNames.length;
127
-
128
- for ( let i = 0; i < numOptions; i++ ) {
129
- const name = optionsNames[i];
130
-
131
- switch ( name ) {
132
- case "onUnsaved" :
133
- case "expose" :
134
- case "promote" :
135
- break;
136
-
137
- default : {
138
- if ( !extracted.options ) {
139
- extracted.options = {};
140
- }
141
-
142
- const option = options[name];
143
-
144
- if ( option != null && typeof option !== "function" ) {
145
- extracted.options[name] = this.cloneSerializable( option );
146
- }
147
- }
148
- }
149
- }
150
-
151
- return extracted;
152
- }
153
-
154
- /**
155
- * Creates deep clone of provided value omitting any data can't be or
156
- * mustn't be serialized.
157
- *
158
- * @param {*} input data to be cloned
159
- * @returns {*} (partial) clone of provided data
160
- */
161
- static cloneSerializable( input ) {
162
- switch ( typeof input ) {
163
- case "object" : {
164
- if ( Array.isArray( input ) ) {
165
- const numItems = input.length;
166
- const clone = new Array( numItems );
167
- let write = 0;
168
-
169
- for ( let read = 0; read < numItems; read++ ) {
170
- const value = this.cloneSerializable( input[read] );
171
-
172
- if ( value != null ) {
173
- clone[write++] = value;
174
- }
175
- }
176
-
177
- clone.splice( write );
178
-
179
- return clone;
180
- }
181
-
182
- const names = Object.keys( input );
183
- const numNames = names.length;
184
- const clone = {};
185
-
186
- for ( let i = 0; i < numNames; i++ ) {
187
- const name = names[i];
188
- const value = this.cloneSerializable( input[name] );
189
-
190
- if ( value != null ) {
191
- clone[name] = value;
192
- }
193
- }
194
-
195
- return clone;
196
- }
197
-
198
- case "function" :
199
- return null;
200
-
201
- default :
202
- return input;
203
- }
204
- }
205
- }
206
-
207
- return OdemRestSchema;
208
- };