@rpcbase/server 0.287.0 → 0.289.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/boot/shared.js CHANGED
@@ -4,7 +4,7 @@ require("./setup_env")
4
4
  require("../src/helpers/expect_ext")
5
5
 
6
6
  const mongoose = require("../mongoose")
7
- require("@rpcbase/access-control")(mongoose)
7
+ require("../access-control")(mongoose)
8
8
 
9
9
  const {database, firebase} = require("../")
10
10
 
package/database.js CHANGED
@@ -52,10 +52,10 @@ module.exports = async(...database_names) => {
52
52
  console.log("connect mongo_url", mongo_url)
53
53
 
54
54
  const connect_opts = {
55
- minPoolSize: 2,
55
+ minPoolSize: 4,
56
56
  // maxPoolSize: 1024 * 1024,
57
57
  // TODO: setting this to a low value adds ~3s delay to requests... WHY
58
- maxPoolSize: 32,
58
+ maxPoolSize: 64,
59
59
  family: 4, // force ipv4
60
60
  driverInfo: {
61
61
  name: `${pack.name}/database`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/server",
3
- "version": "0.287.0",
3
+ "version": "0.289.0",
4
4
  "license": "SSPL-1.0",
5
5
  "main": "./index.js",
6
6
  "bin": {
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,92 @@
1
+ /* @flow */
2
+ const _get = require("lodash/get")
3
+ const colors = require("picocolors")
4
+
5
+ const user_types = ["owner", "user", "any"]
6
+ const perm_operations = ["create", "read", "update", "delete"]
7
+
8
+
9
+ // Owner
10
+ // the creator of the document, or an user_id that is in the "owners" field
11
+
12
+ // User
13
+ // any authenticated user that belongs in this tenant
14
+
15
+ // Any
16
+ // most relaxed permission, any client can access the resource
17
+
18
+
19
+ // given ac config, operation and document / query,
20
+ // check if the user has the permission to run this op
21
+ // 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}`)
24
+ // console.log("rule target", rule_target)
25
+
26
+ // console.log("check_apply_permissions", `'${model_name}:${operation}:${rule_target}'`)
27
+
28
+ if (!rule_target) {
29
+ throw new Error(`undefined rule_target for '${model_name}:${operation}'`)
30
+ }
31
+
32
+ if (!user_types.includes(rule_target)) {
33
+ throw new Error(`expected rule_target '${model_name}:${operation}' to be in '${JSON.stringify(user_types)}', but got '${rule_target}'`)
34
+ }
35
+
36
+ // Create
37
+ if (operation === "create") {
38
+ if (["owner", "user"].includes(rule_target)) {
39
+ if (!user_id) return new Error("create expected authenticated user_id")
40
+ } else {
41
+ return false
42
+ }
43
+ }
44
+ // Read
45
+ else if (operation === "read") {
46
+ const query = item
47
+ if (rule_target === "owner") {
48
+ if (!user_id) throw new Error("read::owner invalid user_id")
49
+
50
+ const conditions = query.getQuery()
51
+
52
+ if (conditions._owners) {
53
+ console.log(colors.yellow("Warning!"), model_name, "ACL for read is owner, but client is sending _owners in query, the field will be overwritten")
54
+ }
55
+ // TODO: implement dynamic ACL based on what the application allows the user to read
56
+ query.setQuery({
57
+ ...conditions,
58
+ _owners: {$in: [user_id]}
59
+ })
60
+
61
+ }
62
+ }
63
+ // Update
64
+ else if (operation === "update") {
65
+ const doc = item
66
+ if (rule_target === "owner") {
67
+ if (!doc._owners.includes(user_id)) {
68
+ // TODO: add debug logging
69
+ return new Error("expected user_id to be in owners to update document")
70
+ }
71
+ } else {
72
+ throw new Error(`unknown rule target '${rule_target}' for operation ${model_name}::update`)
73
+ }
74
+ }
75
+ // Delete
76
+ else if (operation === "delete") {
77
+ const doc = item
78
+ if (rule_target === "owner") {
79
+ if (!doc._owners.includes(user_id)) {
80
+ // TODO: add debug logging
81
+ return new Error("expected user_id to be in owners to delete document")
82
+ }
83
+ } else {
84
+ throw new Error(`unknown rule target '${rule_target}' for operation ${model_name}::delete`)
85
+ }
86
+ }
87
+
88
+ return false
89
+ }
90
+
91
+
92
+ module.exports = check_apply_permissions
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,23 @@
1
+ /* @flow */
2
+
3
+ const get_added_fields = () => {
4
+
5
+ return {
6
+ _owners: {
7
+ type: Array,
8
+ of: String,
9
+ index: true,
10
+ required: true,
11
+ },
12
+ _viewers: [String],
13
+ _editors: [String],
14
+ _created_by: String,
15
+ _created_at: {
16
+ type: Date
17
+ },
18
+ }
19
+
20
+ }
21
+
22
+
23
+ module.exports = get_added_fields
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,26 @@
1
+ /* @flow */
2
+ //
3
+ // const has_permission = require("../has_permission")
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) {
9
+ // const model_name = this.model.modelName
10
+ // const operation = "delete"
11
+ //
12
+ // if (this.op !== "findOneAndDelete") {
13
+ // throw new Error(`in pre_delete unknown operation: ${this.op}`)
14
+ // }
15
+ //
16
+ // const filter = this.getFilter()
17
+ // const doc = await this.model.findOne(filter)
18
+ //
19
+ // console.log("DELETE GOT DOC", doc)
20
+ // // console.log("THIS", this)
21
+ // // console.log("THIS filter", this.getFilter())
22
+ // // console.log("SCHEMA DEL", schema)
23
+ // // TODO: find a way to check if user can delete this document without reading it
24
+ // // console.log("delete got ac config", ac_config)
25
+ // next()
26
+ // }
@@ -0,0 +1,29 @@
1
+ /* @flow */
2
+
3
+ const check_apply_permissions = require("../check_apply_permissions")
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) {
9
+ const model_name = this.model.modelName
10
+ const operation = "delete"
11
+
12
+ if (this.op !== "findOneAndDelete") {
13
+ throw new Error(`in pre_delete unknown operation: ${this.op}`)
14
+ }
15
+
16
+ const user_id = this.options.ctx.req.session.user_id
17
+
18
+ const filter = this.getFilter()
19
+ const doc = await this.model.findOne(filter)
20
+
21
+ // check if user has permission to delete
22
+ const err = check_apply_permissions(ac_config, model_name, "delete", user_id, doc)
23
+ if (err) {
24
+ console.error(err)
25
+ return
26
+ }
27
+
28
+ next()
29
+ }
@@ -0,0 +1,9 @@
1
+ /* @flow */
2
+ const mongoose_plugin = require("./mongoose_plugin")
3
+ const get_config = require("./get_config")
4
+
5
+ module.exports = (mongoose) => {
6
+ const config = get_config()
7
+ const plugin = mongoose_plugin(config)
8
+ mongoose.plugin(plugin)
9
+ }
@@ -0,0 +1,131 @@
1
+ /* @flow */
2
+ const debug = require("debug")
3
+
4
+ const get_added_fields = require("./get_added_fields")
5
+ const check_apply_permissions = require("./check_apply_permissions")
6
+
7
+ // hooks
8
+ const query_pre_delete = require("./hooks/query_pre_delete")
9
+
10
+ const log = debug("rb")
11
+
12
+
13
+ const QUERY = {document: false, query: true}
14
+ const DOC = {document: true, query: false}
15
+
16
+ const get_query_middleware = (op) => (ac_config, schema) => function(next, save_options) {
17
+
18
+ // TODO: this is wrong (AND BREAKS ACL)
19
+ // when no save options, it's a sub schema, we don't want acl on those
20
+ // if (!save_options) {
21
+ // next()
22
+ // console.log("has returned")
23
+ // return
24
+ // }
25
+
26
+ const model_name = this.model.modelName
27
+ if (!model_name) throw new Error("cannot find model_name for query")
28
+
29
+ const options = this.getOptions()
30
+ const user_id = options.ctx?.req?.session?.user_id
31
+
32
+ // client requests should always be authenticated
33
+ if (options.is_client && !user_id) {
34
+ throw new Error("expected user_id in client request")
35
+ }
36
+ // skip if no ctx::user_id (=> is from admin)
37
+ else if (!user_id) {
38
+ log("mongoose_plugin: NO USER ID, skipping")
39
+ return next()
40
+ }
41
+
42
+ const err = check_apply_permissions(ac_config, model_name, "read", user_id, this)
43
+ if (err) {
44
+ console.warn(err)
45
+ return
46
+ }
47
+
48
+ log("access-control will continue")
49
+ next()
50
+ }
51
+
52
+
53
+ // https://mongoosejs.com/docs/middleware.html#types-of-middleware
54
+ const mongoose_plugin = (ac_config) => function rb_acl_plugin(schema, options) {
55
+ schema.options.strict = false
56
+
57
+ // TODO:
58
+ // DANGER: strictQuery to true silently DROPS filter params
59
+ // which can be a critical security risk
60
+ schema.options.strictQuery = false
61
+
62
+ if (!schema.options.isSubSchema) {
63
+ // Add Access Control fields to top level schemas
64
+ schema.add(get_added_fields())
65
+ }
66
+
67
+ // 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))
70
+ // TODO: add countDocuments, estimatedDocumentCount
71
+ // aggregate
72
+
73
+ schema.pre("findOneAndDelete", QUERY, query_pre_delete(ac_config, schema))
74
+
75
+ // Documents create and save
76
+ schema.pre("save", DOC, function(next, options) {
77
+ if (this.$isSubdocument) {
78
+ return next()
79
+ }
80
+
81
+ const model_name = this.constructor.modelName
82
+ if (!model_name) {
83
+ throw new Error("doc pre save model_name is undefined")
84
+ }
85
+
86
+ const {ctx} = options
87
+
88
+ // when no context, assume admin mode and authorize op
89
+ if (!ctx) {
90
+ this._created_at = new Date
91
+ next()
92
+ }
93
+
94
+ const user_id = ctx.req.session?.user_id
95
+
96
+ const doc = this
97
+ // Create
98
+ // TODO: apply create fields
99
+ if (this.isNew) {
100
+ const err = check_apply_permissions(ac_config, model_name, "create", user_id, doc)
101
+ if (err) {
102
+ console.warn(err)
103
+ return
104
+ }
105
+ if (!this._owners.includes(user_id)) {
106
+ this._owners.push(user_id)
107
+ }
108
+ this._created_by = user_id
109
+ this._created_at = new Date
110
+ next()
111
+ }
112
+ // Update
113
+ else {
114
+ const err = check_apply_permissions(ac_config, model_name, "update", user_id, doc)
115
+ if (err) {
116
+ console.warn(err)
117
+ return
118
+ }
119
+ // TODO: apply update fields
120
+ console.log("warning, update check permission not verified")
121
+ next()
122
+ }
123
+ })
124
+
125
+ schema.pre("remove", DOC, function(next) {
126
+ console.log("schema pre REMOVE", this)
127
+ next()
128
+ })
129
+ }
130
+
131
+ module.exports = mongoose_plugin