@rpcbase/server 0.372.0 → 0.374.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.
package/mongoose/index.ts CHANGED
@@ -1,8 +1,10 @@
1
+ // eslint-disable-next-line @rpcbase/lint/no-rb-restricted-imports
1
2
  import mongoose from "mongoose"
2
3
  import {object_id_plugin} from "./plugins/object_id_plugin"
3
4
  import {disable_default_version_key_plugin} from "./plugins/disable_default_version_key_plugin"
4
5
  import {disable_default_timestamps_plugin} from "./plugins/disable_default_timestamps_plugin"
5
6
 
7
+
6
8
  mongoose.set("strictQuery", false)
7
9
 
8
10
  mongoose.plugin(object_id_plugin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.372.0",
3
+ "version": "0.374.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "scripts": {
@@ -61,28 +61,28 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@elastic/elasticsearch": "8.15.0",
64
- "@sentry/node": "8.28.0",
64
+ "@sentry/node": "8.33.1",
65
65
  "bluebird": "3.7.2",
66
- "body-parser": "1.20.2",
67
- "bull": "4.16.2",
66
+ "body-parser": "1.20.3",
67
+ "bull": "4.16.3",
68
68
  "connect-redis": "7.1.1",
69
69
  "cors": "2.8.5",
70
- "debug": "4.3.6",
70
+ "debug": "4.3.7",
71
71
  "dotenv": "16.4.5",
72
- "express": "4.19.2",
72
+ "express": "4.21.0",
73
73
  "express-session": "1.18.0",
74
- "firebase-admin": "12.4.0",
74
+ "firebase-admin": "12.6.0",
75
75
  "lodash": "4.17.21",
76
76
  "mkdirp": "3.0.1",
77
- "mongoose": "8.6.1",
78
- "openai": "4.57.3",
79
- "pdf2pic": "3.1.1",
77
+ "mongoose": "8.7.0",
78
+ "openai": "4.67.1",
79
+ "pdf2pic": "3.1.3",
80
80
  "picocolors": "1.1.0",
81
81
  "postmark": "4.0.5",
82
82
  "redis": "4.7.0",
83
83
  "request-ip": "3.3.0",
84
84
  "sift": "17.1.3",
85
- "socket.io": "4.7.5",
85
+ "socket.io": "4.8.0",
86
86
  "validator": "13.12.0"
87
87
  }
88
88
  }
@@ -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,20 +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
10
 
11
- const log = debug("rb")
12
-
11
+ const log = debug("rb:acl")
13
12
 
14
13
  const QUERY = {document: false, query: true}
15
- const DOC = {document: true, query: false}
14
+ const DOC_OPTIONS = {document: true, query: false}
16
15
 
17
- const get_query_middleware = (op) => (ac_config, schema) => function(next, save_options) {
16
+ const get_query_middleware = (op) => (schema) => async function(next, save_options) {
18
17
 
19
18
  // TODO: this is wrong (AND BREAKS ACL)
20
19
  // when no save options, it's a sub schema, we don't want acl on those
@@ -24,13 +23,18 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
24
23
  // return
25
24
  // }
26
25
 
26
+ const collection_name = this.model.collection.name
27
27
  const model_name = this.model.modelName
28
- 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")
29
31
 
30
32
  const options = this.getOptions()
31
33
  const user_id = options.ctx?.req?.session?.user_id
32
34
 
33
- // client requests should always be authenticated
35
+ // console
36
+
37
+ // client requests should always be authenticated?
34
38
  if (options.is_client && !user_id) {
35
39
  throw new Error("expected user_id in client request")
36
40
  }
@@ -40,10 +44,10 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
40
44
  return next()
41
45
  }
42
46
 
43
- const err = check_apply_permissions(ac_config, model_name, "read", user_id, this)
44
- if (err) {
45
- console.warn(err)
46
- 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")
47
51
  }
48
52
 
49
53
  log("access-control will continue")
@@ -52,7 +56,7 @@ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_
52
56
 
53
57
 
54
58
  // https://mongoosejs.com/docs/middleware.html#types-of-middleware
55
- const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
59
+ const mongoose_plugin = async function(schema, options) {
56
60
  // TODO: should strict be true here??
57
61
  schema.options.strict = false
58
62
  // TODO:
@@ -67,23 +71,23 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
67
71
  }
68
72
 
69
73
  // Queries
70
- schema.pre("find", QUERY, get_query_middleware("find")(ac_config, schema))
71
- 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))
72
76
  // TODO: add countDocuments, estimatedDocumentCount
73
77
  // aggregate
74
-
75
- schema.pre("findOneAndDelete", QUERY, query_pre_delete(ac_config, schema))
78
+ schema.pre("findOneAndDelete", QUERY, query_pre_delete(schema))
76
79
 
77
80
  // Documents create and save
78
- schema.pre("save", DOC, function(next, save_options) {
81
+ schema.pre("save", DOC_OPTIONS, async function(next, save_options) {
79
82
  if (this.$isSubdocument) {
80
83
  return next()
81
84
  }
82
85
 
83
86
  const model_name = this.constructor.modelName
84
- if (!model_name) {
85
- throw new Error("doc pre save model_name is undefined")
86
- }
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")
87
91
 
88
92
  const {ctx} = save_options
89
93
 
@@ -95,11 +99,12 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
95
99
 
96
100
  const user_id = ctx.req.session?.user_id
97
101
 
102
+ const fields = this.modifiedPaths({includeChildren: true})
103
+
98
104
  const doc = this
99
105
  // Create
100
- // TODO: apply create fields
101
106
  if (this.isNew) {
102
- 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})
103
108
  if (err) {
104
109
  console.warn(err)
105
110
  return
@@ -113,7 +118,7 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
113
118
  }
114
119
  // Update
115
120
  else {
116
- 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})
117
122
  if (err) {
118
123
  console.warn(err)
119
124
  return
@@ -122,7 +127,7 @@ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
122
127
  }
123
128
  })
124
129
 
125
- schema.pre("remove", DOC, function(next) {
130
+ schema.pre("remove", DOC_OPTIONS, function(next) {
126
131
  console.log("schema pre REMOVE", this)
127
132
  next()
128
133
  })
@@ -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