@rpcbase/server 0.372.0 → 0.373.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/package.json +11 -11
- package/src/access-control/{check_apply_permissions.js → apply_policies.js} +17 -10
- package/src/access-control/get_policies.js +29 -0
- package/src/access-control/hooks/doc_pre_create.js +1 -1
- package/src/access-control/hooks/query_pre_delete.js +7 -6
- package/src/access-control/index.js +1 -4
- package/src/access-control/mongoose_plugin.js +30 -25
- package/src/models/Policy.ts +13 -0
- package/src/models/index.js +1 -0
- package/src/access-control/access-control.schema.json +0 -21
- package/src/access-control/default-access-control.json +0 -20
- package/src/access-control/get_config.js +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.373.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.
|
|
64
|
+
"@sentry/node": "8.33.1",
|
|
65
65
|
"bluebird": "3.7.2",
|
|
66
|
-
"body-parser": "1.20.
|
|
67
|
-
"bull": "4.16.
|
|
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.
|
|
70
|
+
"debug": "4.3.7",
|
|
71
71
|
"dotenv": "16.4.5",
|
|
72
|
-
"express": "4.
|
|
72
|
+
"express": "4.21.0",
|
|
73
73
|
"express-session": "1.18.0",
|
|
74
|
-
"firebase-admin": "12.
|
|
74
|
+
"firebase-admin": "12.6.0",
|
|
75
75
|
"lodash": "4.17.21",
|
|
76
76
|
"mkdirp": "3.0.1",
|
|
77
|
-
"mongoose": "8.
|
|
78
|
-
"openai": "4.
|
|
79
|
-
"pdf2pic": "3.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.
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 = (
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
7
|
-
const plugin = mongoose_plugin(config)
|
|
8
|
-
mongoose.plugin(plugin)
|
|
5
|
+
mongoose.plugin(mongoose_plugin)
|
|
9
6
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
14
|
+
const DOC_OPTIONS = {document: true, query: false}
|
|
16
15
|
|
|
17
|
-
const get_query_middleware = (op) => (
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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")(
|
|
71
|
-
schema.pre("findOne", QUERY, get_query_middleware("findOne")(
|
|
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",
|
|
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
|
-
|
|
85
|
-
|
|
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 =
|
|
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 =
|
|
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",
|
|
130
|
+
schema.pre("remove", DOC_OPTIONS, function(next) {
|
|
126
131
|
console.log("schema pre REMOVE", this)
|
|
127
132
|
next()
|
|
128
133
|
})
|
package/src/models/index.js
CHANGED
|
@@ -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
|