@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.
- package/mongoose/plugins/object_id_plugin.ts +5 -3
- package/package.json +1 -1
- 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 +33 -26
- package/src/api/stored-values/get_stored_values.js +6 -2
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import mongoose, {
|
|
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
|
|
25
|
+
schema.pre("save", function(next) {
|
|
24
26
|
if (!this._id) {
|
|
25
27
|
this._id = get_object_id()
|
|
26
28
|
}
|
package/package.json
CHANGED
|
@@ -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,19 +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
|
-
const log = debug("rb")
|
|
11
10
|
|
|
11
|
+
const log = debug("rb:acl")
|
|
12
12
|
|
|
13
13
|
const QUERY = {document: false, query: true}
|
|
14
|
-
const
|
|
14
|
+
const DOC_OPTIONS = {document: true, query: false}
|
|
15
15
|
|
|
16
|
-
const get_query_middleware = (op) => (
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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
|
|
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")(
|
|
69
|
-
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))
|
|
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",
|
|
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
|
-
|
|
83
|
-
|
|
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} =
|
|
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 =
|
|
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 =
|
|
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",
|
|
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(
|
|
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 = {
|
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
|