@rpcbase/server 0.370.0 → 0.372.0-aclpolicies.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.
@@ -1,6 +1,7 @@
1
- import mongoose, { Schema } from "../"
1
+ import mongoose, {Schema} from "../"
2
+
3
+ import {get_object_id} from "../../get_object_id"
2
4
 
3
- import { get_object_id } from "../../get_object_id"
4
5
 
5
6
  export const object_id_plugin = (schema: Schema) => {
6
7
  if (schema.options._id === false) {
@@ -16,11 +17,12 @@ export const object_id_plugin = (schema: Schema) => {
16
17
  _id: {
17
18
  type: mongoose.Schema.Types.ObjectId,
18
19
  default: () => get_object_id(),
20
+ immutable: true,
19
21
  },
20
22
  })
21
23
 
22
24
  // Optional: Ensure the _id field is always set
23
- schema.pre("save", function (next) {
25
+ schema.pre("save", function(next) {
24
26
  if (!this._id) {
25
27
  this._id = get_object_id()
26
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.370.0",
3
+ "version": "0.372.0-aclpolicies.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "scripts": {
@@ -2,10 +2,16 @@
2
2
  const _get = require("lodash/get")
3
3
  const colors = require("picocolors")
4
4
 
5
+ const get_policies = require("./get_policies")
6
+
7
+
5
8
  const user_types = ["owner", "user", "any"]
6
9
  const perm_operations = ["create", "read", "update", "delete"]
7
10
 
8
11
 
12
+ // TODO: add LRU cache for policies
13
+
14
+
9
15
  // Owner
10
16
  // the creator of the document, or an user_id that is in the "owners" field
11
17
 
@@ -19,11 +25,16 @@ const perm_operations = ["create", "read", "update", "delete"]
19
25
  // given ac config, operation and document / query,
20
26
  // check if the user has the permission to run this op
21
27
  // if the user doesn't have permission, the system should throw an error
22
- const check_apply_permissions = (ac_config, model_name, operation, user_id, item) => {
23
- const rule_target = _get(ac_config, `${model_name}.${operation}`)
28
+ const apply_policies = async({collection_name, model_name, operation, fields, user_id, doc}) => {
29
+ // TODO: fix rm policy here
30
+ // const rule_target = _get(ac_config, `${model_name}.${operation}`)
31
+ const rule_target = ""
24
32
  // console.log("rule target", rule_target)
25
33
 
26
- // console.log("check_apply_permissions", `'${model_name}:${operation}:${rule_target}'`)
34
+ const policies = await get_policies({collection_name, model_name, operation, fields, user_id, doc})
35
+ console.log("MODEL APPLY POLICIES", {collection_name, model_name, operation, fields, user_id, doc})
36
+ return
37
+ // console.log("apply_policies", `'${model_name}:${operation}:${rule_target}'`)
27
38
 
28
39
  if (!rule_target) {
29
40
  throw new Error(`undefined rule_target for '${model_name}:${operation}'`)
@@ -43,11 +54,10 @@ const check_apply_permissions = (ac_config, model_name, operation, user_id, item
43
54
  }
44
55
  // Read
45
56
  else if (operation === "read") {
46
- const query = item
47
57
  if (rule_target === "owner") {
48
58
  if (!user_id) throw new Error("read::owner invalid user_id")
49
59
 
50
- const conditions = query.getQuery()
60
+ const conditions = doc.getQuery()
51
61
 
52
62
  // TMP: warn if user supplied _owners, which is currently overwritten for ACL
53
63
  if (conditions._owners) {
@@ -56,8 +66,7 @@ const check_apply_permissions = (ac_config, model_name, operation, user_id, item
56
66
  }
57
67
  }
58
68
 
59
- // TODO: implement dynamic ACL based on what the application allows the user to read
60
- query.setQuery({
69
+ doc.setQuery({
61
70
  ...conditions,
62
71
  _owners: {$in: [user_id]}
63
72
  })
@@ -66,7 +75,6 @@ const check_apply_permissions = (ac_config, model_name, operation, user_id, item
66
75
  }
67
76
  // Update
68
77
  else if (operation === "update") {
69
- const doc = item
70
78
  if (rule_target === "owner") {
71
79
  if (!doc._owners?.includes(user_id)) {
72
80
  console.log("MODEL:", model_name)
@@ -80,7 +88,6 @@ const check_apply_permissions = (ac_config, model_name, operation, user_id, item
80
88
  }
81
89
  // Delete
82
90
  else if (operation === "delete") {
83
- const doc = item
84
91
  if (rule_target === "owner") {
85
92
  if (!doc._owners.includes(user_id)) {
86
93
  // TODO: add debug logging
@@ -95,4 +102,4 @@ const check_apply_permissions = (ac_config, model_name, operation, user_id, item
95
102
  }
96
103
 
97
104
 
98
- module.exports = check_apply_permissions
105
+ module.exports = apply_policies
@@ -0,0 +1,29 @@
1
+ const Policy = require("../models/Policy")
2
+
3
+ const DEFAULT_POLICY = {
4
+ "create": "user",
5
+ "read": "owner",
6
+ "update": "owner",
7
+ "delete": "owner",
8
+ }
9
+
10
+ const get_policies = async({collection_name, model_name, operation, user_id, doc}) => {
11
+
12
+ const policies = await Policy.find({
13
+ $or: [
14
+ { collection_name, operations: { $in: [operation] }},
15
+ { doc_id: doc._id, operations: { $in: [operation] }},
16
+ // TODO: add field level operations
17
+ ]
18
+ })
19
+
20
+ if (!policies) {
21
+ return [DEFAULT_POLICY]
22
+ }
23
+
24
+ console.log("POLICIES", policies)
25
+
26
+ return policies
27
+ }
28
+
29
+ module.exports = get_policies
@@ -5,7 +5,7 @@
5
5
  // const delay = (time) => new Promise((resolve) => setTimeout(resolve, time))
6
6
  // // await delay(2000)
7
7
  //
8
- // module.exports = (ac_config, schema) => async function(next) {
8
+ // module.exports = (schema) => async function(next) {
9
9
  // const model_name = this.model.modelName
10
10
  // const operation = "delete"
11
11
  //
@@ -1,12 +1,13 @@
1
1
  /* @flow */
2
+ const apply_policies = require("../apply_policies")
2
3
 
3
- const check_apply_permissions = require("../check_apply_permissions")
4
4
 
5
- // const delay = (time) => new Promise((resolve) => setTimeout(resolve, time))
6
- // await delay(2000)
7
-
8
- module.exports = (ac_config, schema) => async function(next) {
5
+ module.exports = (schema) => async function(next) {
9
6
  const model_name = this.model.modelName
7
+ const collection_name = this.model.collection.name
8
+
9
+ console.log("DELETE PLUGIN GET OPTIONS", this.getOptions())
10
+
10
11
  const operation = "delete"
11
12
 
12
13
  if (this.op !== "findOneAndDelete") {
@@ -19,7 +20,7 @@ module.exports = (ac_config, schema) => async function(next) {
19
20
  const doc = await this.model.findOne(filter)
20
21
 
21
22
  // check if user has permission to delete
22
- const err = check_apply_permissions(ac_config, model_name, "delete", user_id, doc)
23
+ const err = await apply_policies({collection_name, model_name, operation, user_id, doc})
23
24
  if (err) {
24
25
  console.error(err)
25
26
  return
@@ -1,9 +1,6 @@
1
1
  /* @flow */
2
2
  const mongoose_plugin = require("./mongoose_plugin")
3
- const get_config = require("./get_config")
4
3
 
5
4
  module.exports = (mongoose) => {
6
- const config = get_config()
7
- const plugin = mongoose_plugin(config)
8
- mongoose.plugin(plugin)
5
+ mongoose.plugin(mongoose_plugin)
9
6
  }
@@ -1,19 +1,19 @@
1
- /* @flow */
1
+ const assert = require("assert")
2
2
  const debug = require("debug")
3
3
 
4
4
  const get_added_fields = require("./get_added_fields")
5
- const check_apply_permissions = require("./check_apply_permissions")
5
+ const apply_policies = require("./apply_policies")
6
6
 
7
7
  // hooks
8
8
  const query_pre_delete = require("./hooks/query_pre_delete")
9
9
 
10
- const log = debug("rb")
11
10
 
11
+ const log = debug("rb:acl")
12
12
 
13
13
  const QUERY = {document: false, query: true}
14
- const DOC = {document: true, query: false}
14
+ const DOC_OPTIONS = {document: true, query: false}
15
15
 
16
- const get_query_middleware = (op) => (ac_config, schema) => function(next, save_options) {
16
+ const get_query_middleware = (op) => (schema) => async function(next, save_options) {
17
17
 
18
18
  // TODO: this is wrong (AND BREAKS ACL)
19
19
  // when no save options, it's a sub schema, we don't want acl on those
@@ -23,13 +23,18 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
23
23
  // return
24
24
  // }
25
25
 
26
+ const collection_name = this.model.collection.name
26
27
  const model_name = this.model.modelName
27
- if (!model_name) throw new Error("cannot find model_name for query")
28
+
29
+ assert(model_name, "cannot find model_name for query")
30
+ assert(collection_name, "cannot find collection_name for query")
28
31
 
29
32
  const options = this.getOptions()
30
33
  const user_id = options.ctx?.req?.session?.user_id
31
34
 
32
- // client requests should always be authenticated
35
+ // console
36
+
37
+ // client requests should always be authenticated?
33
38
  if (options.is_client && !user_id) {
34
39
  throw new Error("expected user_id in client request")
35
40
  }
@@ -39,10 +44,10 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
39
44
  return next()
40
45
  }
41
46
 
42
- const err = check_apply_permissions(ac_config, model_name, "read", user_id, this)
43
- if (err) {
44
- console.warn(err)
45
- return
47
+ const errors = await apply_policies({collection_name, model_name, operation: "read", user_id, doc: this})
48
+
49
+ if (errors?.length > 0) {
50
+ throw new AggregateError(errors, "access-control policies error")
46
51
  }
47
52
 
48
53
  log("access-control will continue")
@@ -51,7 +56,7 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
51
56
 
52
57
 
53
58
  // https://mongoosejs.com/docs/middleware.html#types-of-middleware
54
- const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
59
+ const mongoose_plugin = async function(schema, options) {
55
60
  // TODO: should strict be true here??
56
61
  schema.options.strict = false
57
62
  // TODO:
@@ -59,31 +64,32 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
59
64
  // which can be a critical security risk
60
65
  schema.options.strictQuery = false
61
66
 
62
- if (!schema.options.isSubSchema) {
67
+ // TODO: acl should be explicitly on by default and only if set to false in schema definition we remove it
68
+ if (!schema.options.isSubSchema && schema.options.acl !== false) {
63
69
  // Add Access Control fields to top level schemas
64
70
  schema.add(get_added_fields())
65
71
  }
66
72
 
67
73
  // Queries
68
- schema.pre("find", QUERY, get_query_middleware("find")(ac_config, schema))
69
- schema.pre("findOne", QUERY, get_query_middleware("findOne")(ac_config, schema))
74
+ schema.pre("find", QUERY, get_query_middleware("find")(schema))
75
+ schema.pre("findOne", QUERY, get_query_middleware("findOne")(schema))
70
76
  // TODO: add countDocuments, estimatedDocumentCount
71
77
  // aggregate
72
-
73
- schema.pre("findOneAndDelete", QUERY, query_pre_delete(ac_config, schema))
78
+ schema.pre("findOneAndDelete", QUERY, query_pre_delete(schema))
74
79
 
75
80
  // Documents create and save
76
- schema.pre("save", DOC, function(next, options) {
81
+ schema.pre("save", DOC_OPTIONS, async function(next, save_options) {
77
82
  if (this.$isSubdocument) {
78
83
  return next()
79
84
  }
80
85
 
81
86
  const model_name = this.constructor.modelName
82
- if (!model_name) {
83
- throw new Error("doc pre save model_name is undefined")
84
- }
87
+ assert(model_name, "doc pre save model_name is undefined")
88
+
89
+ const collection_name = this.constructor.collection.name
90
+ assert(collection_name, "doc pre save collection_name is undefined")
85
91
 
86
- const {ctx} = options
92
+ const {ctx} = save_options
87
93
 
88
94
  // when no context, assume admin mode and authorize op
89
95
  if (!ctx) {
@@ -93,11 +99,12 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
93
99
 
94
100
  const user_id = ctx.req.session?.user_id
95
101
 
102
+ const fields = this.modifiedPaths({includeChildren: true})
103
+
96
104
  const doc = this
97
105
  // Create
98
- // TODO: apply create fields
99
106
  if (this.isNew) {
100
- const err = check_apply_permissions(ac_config, model_name, "create", user_id, doc)
107
+ const err = await apply_policies({collection_name, model_name, operation: "create", fields, user_id, doc})
101
108
  if (err) {
102
109
  console.warn(err)
103
110
  return
@@ -111,7 +118,7 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
111
118
  }
112
119
  // Update
113
120
  else {
114
- const err = check_apply_permissions(ac_config, model_name, "update", user_id, doc)
121
+ const err = await apply_policies({collection_name, model_name, operation: "update", fields, user_id, doc})
115
122
  if (err) {
116
123
  console.warn(err)
117
124
  return
@@ -120,7 +127,7 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
120
127
  }
121
128
  })
122
129
 
123
- schema.pre("remove", DOC, function(next) {
130
+ schema.pre("remove", DOC_OPTIONS, function(next) {
124
131
  console.log("schema pre REMOVE", this)
125
132
  next()
126
133
  })
@@ -14,14 +14,18 @@ const get_projection = (payload) => {
14
14
  return projection
15
15
  }
16
16
 
17
-
18
17
  const get_stored_values = async(payload, ctx) => {
19
18
  const {user_id} = ctx.req.session
20
19
  expect(user_id).toBeMongoId()
21
20
 
22
21
  const projection = get_projection(payload)
23
22
 
24
- const storage_doc = await UserStoredValues.findOne({_owners: {$in: [user_id]}}, projection, {ctx})
23
+ const storage_doc = await UserStoredValues.findOne(
24
+ {_owners: {$in: [user_id]}},
25
+ projection,
26
+ {ctx},
27
+ )
28
+
25
29
  assert(storage_doc, `unable to retrieve storage_doc for user: ${user_id}`)
26
30
 
27
31
  const result = {
@@ -0,0 +1,13 @@
1
+ const mongoose = require("../../mongoose")
2
+
3
+ const Policy = mongoose.model("Policy", {
4
+ user_id: String,
5
+ token_hash: String,
6
+ created_at: {
7
+ type: Date,
8
+ expires: 360, // 6min
9
+ default: Date.now,
10
+ }
11
+ })
12
+
13
+ module.exports = Policy
@@ -1,5 +1,6 @@
1
1
  require("./Invite")
2
2
  require("./Notification")
3
+ require("./Policy")
3
4
  require("./ResetPasswordToken")
4
5
  require("./User")
5
6
  require("./UserStoredValues")
@@ -1,21 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://schemas.rpcbase.com/schemas/aceess-control.schema.json",
4
- "title": "access-control",
5
- "type": "object",
6
- "properties": {
7
- "prop1": {
8
- "type": "string",
9
- "description": "The person's first name."
10
- },
11
- "lastName": {
12
- "type": "string",
13
- "description": "The person's last name."
14
- },
15
- "age": {
16
- "description": "Age in years which must be equal to or greater than zero.",
17
- "type": "integer",
18
- "minimum": 0
19
- }
20
- }
21
- }
@@ -1,20 +0,0 @@
1
- {
2
- "Invite": {
3
- "create": "any",
4
- "read": "owner",
5
- "update": "owner",
6
- "delete": "owner"
7
- },
8
- "User": {
9
- "create": "any",
10
- "read": "owner",
11
- "update": "owner",
12
- "delete": "owner"
13
- },
14
- "UserStoredValues": {
15
- "create": "any",
16
- "read": "owner",
17
- "update": "owner",
18
- "delete": "owner"
19
- }
20
- }
@@ -1,34 +0,0 @@
1
- /* @flow */
2
- const path = require("path")
3
- const fs = require("fs")
4
-
5
- const default_config = require("./default-access-control.json")
6
-
7
-
8
- const validate_config = (models_dir, config) => {
9
- // for each key in config, check if model exists
10
- // check if config parameters also exist
11
- console.log("access-control:NYI: validate config", models_dir)
12
- }
13
-
14
-
15
- // this assumes the project is ran from the server/server/ directory
16
- const get_config = (custom_path) => {
17
- const config_path = custom_path || path.join(process.cwd(), "./src/models/access-control.json")
18
-
19
- if (!fs.existsSync(config_path)) {
20
- console.log("config path does not exist", config_path)
21
- }
22
-
23
- const conf_str = fs.readFileSync(config_path, "utf8")
24
- const config = JSON.parse(conf_str)
25
-
26
- const models_dir = path.dirname(config_path)
27
- validate_config(models_dir, config)
28
-
29
- return Object.assign(config, default_config)
30
- }
31
-
32
-
33
-
34
- module.exports = get_config