@teamkeel/functions-runtime 0.321.1 → 0.321.2
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 +1 -1
- package/pnpm-lock.yaml +742 -332
- package/src/ModelAPI.js +14 -9
- package/src/ModelAPI.test.js +0 -3
- package/src/database.js +10 -0
- package/src/handleRequest.js +80 -77
- package/src/handleRequest.test.js +42 -73
- package/src/permissions.js +13 -21
- package/src/permissions.test.js +31 -28
package/src/ModelAPI.js
CHANGED
|
@@ -40,11 +40,9 @@ class ModelAPI {
|
|
|
40
40
|
/**
|
|
41
41
|
* @param {string} tableName The name of the table this API is for
|
|
42
42
|
* @param {Function} defaultValues A function that returns the default values for a row in this table
|
|
43
|
-
* @param {import("kysely").Kysely} db
|
|
44
43
|
* @param {TableConfigMap} tableConfigMap
|
|
45
44
|
*/
|
|
46
|
-
constructor(tableName, defaultValues,
|
|
47
|
-
this._db = db || getDatabase();
|
|
45
|
+
constructor(tableName, defaultValues, tableConfigMap = {}) {
|
|
48
46
|
this._defaultValues = defaultValues;
|
|
49
47
|
this._tableName = tableName;
|
|
50
48
|
this._tableConfigMap = tableConfigMap;
|
|
@@ -53,11 +51,12 @@ class ModelAPI {
|
|
|
53
51
|
|
|
54
52
|
async create(values) {
|
|
55
53
|
const name = spanName(this._modelName, "create");
|
|
54
|
+
const db = getDatabase();
|
|
56
55
|
|
|
57
56
|
return tracing.withSpan(name, async (span) => {
|
|
58
57
|
try {
|
|
59
58
|
const defaults = this._defaultValues();
|
|
60
|
-
const query =
|
|
59
|
+
const query = db
|
|
61
60
|
.insertInto(this._tableName)
|
|
62
61
|
.values(
|
|
63
62
|
snakeCaseObject({
|
|
@@ -79,9 +78,10 @@ class ModelAPI {
|
|
|
79
78
|
|
|
80
79
|
async findOne(where = {}) {
|
|
81
80
|
const name = spanName(this._modelName, "findOne");
|
|
81
|
+
const db = getDatabase();
|
|
82
82
|
|
|
83
83
|
return tracing.withSpan(name, async (span) => {
|
|
84
|
-
let builder =
|
|
84
|
+
let builder = db
|
|
85
85
|
.selectFrom(this._tableName)
|
|
86
86
|
.distinctOn(`${this._tableName}.id`)
|
|
87
87
|
.selectAll(this._tableName);
|
|
@@ -103,9 +103,10 @@ class ModelAPI {
|
|
|
103
103
|
|
|
104
104
|
async findMany(where = {}) {
|
|
105
105
|
const name = spanName(this._modelName, "findMany");
|
|
106
|
+
const db = getDatabase();
|
|
106
107
|
|
|
107
108
|
return tracing.withSpan(name, async (span) => {
|
|
108
|
-
let builder =
|
|
109
|
+
let builder = db
|
|
109
110
|
.selectFrom(this._tableName)
|
|
110
111
|
.distinctOn(`${this._tableName}.id`)
|
|
111
112
|
.selectAll(this._tableName);
|
|
@@ -124,9 +125,10 @@ class ModelAPI {
|
|
|
124
125
|
|
|
125
126
|
async update(where, values) {
|
|
126
127
|
const name = spanName(this._modelName, "update");
|
|
128
|
+
const db = getDatabase();
|
|
127
129
|
|
|
128
130
|
return tracing.withSpan(name, async (span) => {
|
|
129
|
-
let builder =
|
|
131
|
+
let builder = db.updateTable(this._tableName).returningAll();
|
|
130
132
|
|
|
131
133
|
builder = builder.set(snakeCaseObject(values));
|
|
132
134
|
|
|
@@ -148,9 +150,10 @@ class ModelAPI {
|
|
|
148
150
|
|
|
149
151
|
async delete(where) {
|
|
150
152
|
const name = spanName(this._modelName, "delete");
|
|
153
|
+
const db = getDatabase();
|
|
151
154
|
|
|
152
155
|
return tracing.withSpan(name, async (span) => {
|
|
153
|
-
let builder =
|
|
156
|
+
let builder = db.deleteFrom(this._tableName).returning(["id"]);
|
|
154
157
|
|
|
155
158
|
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
156
159
|
|
|
@@ -168,7 +171,9 @@ class ModelAPI {
|
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
where(where) {
|
|
171
|
-
|
|
174
|
+
const db = getDatabase();
|
|
175
|
+
|
|
176
|
+
let builder = db
|
|
172
177
|
.selectFrom(this._tableName)
|
|
173
178
|
.distinctOn(`${this._tableName}.id`)
|
|
174
179
|
.selectAll(this._tableName);
|
package/src/ModelAPI.test.js
CHANGED
|
@@ -62,7 +62,6 @@ beforeEach(async () => {
|
|
|
62
62
|
date: new Date("2022-01-01"),
|
|
63
63
|
};
|
|
64
64
|
},
|
|
65
|
-
db,
|
|
66
65
|
tableConfigMap
|
|
67
66
|
);
|
|
68
67
|
|
|
@@ -73,7 +72,6 @@ beforeEach(async () => {
|
|
|
73
72
|
id: KSUID.randomSync().string,
|
|
74
73
|
};
|
|
75
74
|
},
|
|
76
|
-
db,
|
|
77
75
|
tableConfigMap
|
|
78
76
|
);
|
|
79
77
|
|
|
@@ -84,7 +82,6 @@ beforeEach(async () => {
|
|
|
84
82
|
id: KSUID.randomSync().string,
|
|
85
83
|
};
|
|
86
84
|
},
|
|
87
|
-
db,
|
|
88
85
|
tableConfigMap
|
|
89
86
|
);
|
|
90
87
|
});
|
package/src/database.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { Kysely, PostgresDialect } = require("kysely");
|
|
2
|
+
const { AsyncLocalStorage } = require("async_hooks");
|
|
2
3
|
const pg = require("pg");
|
|
3
4
|
|
|
4
5
|
function mustEnv(key) {
|
|
@@ -25,8 +26,16 @@ function getDialect() {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
let db = null;
|
|
29
|
+
const dbInstance = new AsyncLocalStorage();
|
|
28
30
|
|
|
31
|
+
// getDatabase will first check for an instance of Kysely in AsyncLocalStorage,
|
|
32
|
+
// otherwise it will create a new instance and reuse it..
|
|
29
33
|
function getDatabase() {
|
|
34
|
+
let fromStore = dbInstance.getStore();
|
|
35
|
+
if (fromStore) {
|
|
36
|
+
return fromStore;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
if (db) {
|
|
31
40
|
return db;
|
|
32
41
|
}
|
|
@@ -38,4 +47,5 @@ function getDatabase() {
|
|
|
38
47
|
return db;
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
module.exports.dbInstance = dbInstance;
|
|
41
51
|
module.exports.getDatabase = getDatabase;
|
package/src/handleRequest.js
CHANGED
|
@@ -3,15 +3,15 @@ const {
|
|
|
3
3
|
createJSONRPCSuccessResponse,
|
|
4
4
|
JSONRPCErrorCode,
|
|
5
5
|
} = require("json-rpc-2.0");
|
|
6
|
-
|
|
7
|
-
const { getDatabase } = require("./database");
|
|
6
|
+
const { getDatabase, dbInstance } = require("./database");
|
|
8
7
|
const {
|
|
9
8
|
PERMISSION_STATE,
|
|
9
|
+
Permissions,
|
|
10
10
|
PermissionError,
|
|
11
11
|
checkBuiltInPermissions,
|
|
12
|
+
permissionsApiInstance,
|
|
12
13
|
} = require("./permissions");
|
|
13
14
|
const { PROTO_ACTION_TYPES } = require("./consts");
|
|
14
|
-
|
|
15
15
|
const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
|
|
16
16
|
const opentelemetry = require("@opentelemetry/api");
|
|
17
17
|
const { getTracer, withSpan } = require("./tracing");
|
|
@@ -31,13 +31,8 @@ async function handleRequest(request, config) {
|
|
|
31
31
|
// Wrapping span for the whole request
|
|
32
32
|
return withSpan(request.method, async (span) => {
|
|
33
33
|
try {
|
|
34
|
-
const {
|
|
35
|
-
|
|
36
|
-
createContextAPI,
|
|
37
|
-
functions,
|
|
38
|
-
permissions,
|
|
39
|
-
actionTypes,
|
|
40
|
-
} = config;
|
|
34
|
+
const { createContextAPI, functions, permissionFns, actionTypes } =
|
|
35
|
+
config;
|
|
41
36
|
|
|
42
37
|
if (!(request.method in functions)) {
|
|
43
38
|
const message = `no corresponding function found for '${request.method}'`;
|
|
@@ -55,77 +50,85 @@ async function handleRequest(request, config) {
|
|
|
55
50
|
// headers reference passed to custom function where object data can be modified
|
|
56
51
|
const headers = new Headers();
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
// The ctx argument passed into the custom function.
|
|
54
|
+
const ctx = createContextAPI({
|
|
55
|
+
responseHeaders: headers,
|
|
56
|
+
meta: request.meta,
|
|
57
|
+
});
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const result = await db.transaction().execute(async (transaction) => {
|
|
65
|
-
const ctx = createContextAPI({
|
|
66
|
-
responseHeaders: headers,
|
|
67
|
-
meta: request.meta,
|
|
68
|
-
});
|
|
69
|
-
const api = createFunctionAPI({
|
|
70
|
-
meta: request.meta,
|
|
71
|
-
db: transaction,
|
|
72
|
-
});
|
|
59
|
+
const permitted =
|
|
60
|
+
request.meta && request.meta.permissionState.status === "granted"
|
|
61
|
+
? true
|
|
62
|
+
: null;
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
64
|
+
const db = getDatabase();
|
|
65
|
+
const permissions = new Permissions();
|
|
66
|
+
|
|
67
|
+
const result = await permissionsApiInstance.run(
|
|
68
|
+
{ permitted: permitted },
|
|
69
|
+
() => {
|
|
70
|
+
// We want to wrap the execution of the custom function in a transaction so that any call the user makes
|
|
71
|
+
// to any of the model apis we provide to the custom function is processed in a transaction.
|
|
72
|
+
// This is useful for permissions where we want to only proceed with database writes if all permission rules
|
|
73
|
+
// have been validated.
|
|
74
|
+
|
|
75
|
+
return db.transaction().execute(async (transaction) => {
|
|
76
|
+
return dbInstance.run(transaction, async () => {
|
|
77
|
+
// Call the user's custom function!
|
|
78
|
+
const customFunction = functions[request.method];
|
|
79
|
+
const fnResult = await customFunction(ctx, request.params);
|
|
80
|
+
|
|
81
|
+
// api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
|
|
82
|
+
// we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
|
|
83
|
+
// and therefore we default to checking the permissions defined in the schema automatically.
|
|
84
|
+
switch (permissions.getState()) {
|
|
85
|
+
case PERMISSION_STATE.PERMITTED:
|
|
86
|
+
return fnResult;
|
|
87
|
+
case PERMISSION_STATE.UNPERMITTED:
|
|
88
|
+
throw new PermissionError(
|
|
89
|
+
`Not permitted to access ${request.method}`
|
|
90
|
+
);
|
|
91
|
+
default:
|
|
92
|
+
// unknown state, proceed with checking against the built in permissions in the schema
|
|
93
|
+
const relevantPermissions = permissionFns[request.method];
|
|
94
|
+
const actionType = actionTypes[request.method];
|
|
95
|
+
|
|
96
|
+
const peakInsideTransaction =
|
|
97
|
+
actionType === PROTO_ACTION_TYPES.CREATE;
|
|
98
|
+
|
|
99
|
+
let rowsForPermissions = [];
|
|
100
|
+
switch (actionType) {
|
|
101
|
+
case PROTO_ACTION_TYPES.LIST:
|
|
102
|
+
rowsForPermissions = fnResult;
|
|
103
|
+
break;
|
|
104
|
+
case PROTO_ACTION_TYPES.DELETE:
|
|
105
|
+
rowsForPermissions = [{ id: fnResult }];
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
rowsForPermissions = [fnResult];
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// check will throw a PermissionError if a permission rule is invalid
|
|
113
|
+
await checkBuiltInPermissions({
|
|
114
|
+
rows: rowsForPermissions,
|
|
115
|
+
permissionFns: relevantPermissions,
|
|
116
|
+
// it is important that we pass db here as db represents the connection to the database
|
|
117
|
+
// *outside* of the current transaction. Given that any changes inside of a transaction
|
|
118
|
+
// are opaque to the outside, we can utilize this when running permission rules and then deciding to
|
|
119
|
+
// rollback any changes if they do not pass. However, for creates we need to be able to 'peak' inside the transaction to read the created record, as this won't exist outside of the transaction.
|
|
120
|
+
db: peakInsideTransaction ? transaction : db,
|
|
121
|
+
ctx,
|
|
122
|
+
functionName: request.method,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// If the built in permission check above doesn't throw, then it means that the request is permitted and we can continue returning the return value from the custom function out of the transaction
|
|
126
|
+
return fnResult;
|
|
127
|
+
}
|
|
123
128
|
});
|
|
124
|
-
|
|
125
|
-
// If the built in permission check above doesn't throw, then it means that the request is permitted and we can continue returning the return value from the custom function out of the transaction
|
|
126
|
-
return fnResult;
|
|
129
|
+
});
|
|
127
130
|
}
|
|
128
|
-
|
|
131
|
+
);
|
|
129
132
|
|
|
130
133
|
if (result === undefined) {
|
|
131
134
|
// no result returned from custom function
|
|
@@ -4,9 +4,8 @@ import { handleRequest, RuntimeErrors } from "./handleRequest";
|
|
|
4
4
|
import { test, expect, beforeEach, describe } from "vitest";
|
|
5
5
|
import { ModelAPI } from "./ModelAPI";
|
|
6
6
|
import { getDatabase } from "./database";
|
|
7
|
-
|
|
7
|
+
const { Permissions } = require("./permissions");
|
|
8
8
|
import { PROTO_ACTION_TYPES } from "./consts";
|
|
9
|
-
|
|
10
9
|
import KSUID from "ksuid";
|
|
11
10
|
|
|
12
11
|
process.env.KEEL_DB_CONN_TYPE = "pg";
|
|
@@ -15,8 +14,9 @@ process.env.KEEL_DB_CONN = `postgresql://postgres:postgres@localhost:5432/functi
|
|
|
15
14
|
test("when the custom function returns expected value", async () => {
|
|
16
15
|
const config = {
|
|
17
16
|
functions: {
|
|
18
|
-
createPost: async (ctx, inputs
|
|
19
|
-
|
|
17
|
+
createPost: async (ctx, inputs) => {
|
|
18
|
+
new Permissions().allow();
|
|
19
|
+
|
|
20
20
|
return {
|
|
21
21
|
title: "a post",
|
|
22
22
|
id: "abcde",
|
|
@@ -26,11 +26,6 @@ test("when the custom function returns expected value", async () => {
|
|
|
26
26
|
actionTypes: {
|
|
27
27
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
28
28
|
},
|
|
29
|
-
createFunctionAPI: ({ db }) => {
|
|
30
|
-
return {
|
|
31
|
-
permissions: new Permissions(),
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
29
|
createContextAPI: () => {},
|
|
35
30
|
};
|
|
36
31
|
|
|
@@ -52,19 +47,14 @@ test("when the custom function returns expected value", async () => {
|
|
|
52
47
|
test("when the custom function doesnt return a value", async () => {
|
|
53
48
|
const config = {
|
|
54
49
|
functions: {
|
|
55
|
-
createPost: async (ctx, inputs
|
|
56
|
-
|
|
50
|
+
createPost: async (ctx, inputs) => {
|
|
51
|
+
new Permissions().allow();
|
|
57
52
|
},
|
|
58
53
|
},
|
|
59
54
|
permissions: {},
|
|
60
55
|
actionTypes: {
|
|
61
56
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
62
57
|
},
|
|
63
|
-
createFunctionAPI: ({ db }) => {
|
|
64
|
-
return {
|
|
65
|
-
permissions: new Permissions(),
|
|
66
|
-
};
|
|
67
|
-
},
|
|
68
58
|
createContextAPI: () => {},
|
|
69
59
|
};
|
|
70
60
|
|
|
@@ -83,16 +73,11 @@ test("when the custom function doesnt return a value", async () => {
|
|
|
83
73
|
test("when there is no matching function for the path", async () => {
|
|
84
74
|
const config = {
|
|
85
75
|
functions: {
|
|
86
|
-
createPost: async (ctx, inputs
|
|
76
|
+
createPost: async (ctx, inputs) => {},
|
|
87
77
|
},
|
|
88
78
|
actionTypes: {
|
|
89
79
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
90
80
|
},
|
|
91
|
-
createFunctionAPI: ({ db }) => {
|
|
92
|
-
return {
|
|
93
|
-
permissions: new Permissions(),
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
81
|
createContextAPI: () => {},
|
|
97
82
|
};
|
|
98
83
|
|
|
@@ -111,20 +96,13 @@ test("when there is no matching function for the path", async () => {
|
|
|
111
96
|
test("when there is an unexpected error in the custom function", async () => {
|
|
112
97
|
const config = {
|
|
113
98
|
functions: {
|
|
114
|
-
createPost: async (ctx, inputs
|
|
115
|
-
api.permissions.allow();
|
|
116
|
-
|
|
99
|
+
createPost: async (ctx, inputs) => {
|
|
117
100
|
throw new Error("oopsie daisy");
|
|
118
101
|
},
|
|
119
102
|
},
|
|
120
103
|
actionTypes: {
|
|
121
104
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
122
105
|
},
|
|
123
|
-
createFunctionAPI: ({ db }) => {
|
|
124
|
-
return {
|
|
125
|
-
permissions: new Permissions(),
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
106
|
createContextAPI: () => {},
|
|
129
107
|
};
|
|
130
108
|
|
|
@@ -152,16 +130,16 @@ test("when a role based permission has already been granted by the main runtime"
|
|
|
152
130
|
actionTypes: {
|
|
153
131
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
154
132
|
},
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
permissions: new Permissions({ status: "granted", reason: "role" }),
|
|
158
|
-
};
|
|
159
|
-
},
|
|
133
|
+
createModelAPI: () => {},
|
|
160
134
|
createContextAPI: () => {},
|
|
161
135
|
};
|
|
162
136
|
|
|
163
|
-
|
|
137
|
+
let rpcReq = createJSONRPCRequest("123", "createPost", { title: "a post" });
|
|
164
138
|
|
|
139
|
+
Object.assign(rpcReq, {
|
|
140
|
+
...rpcReq,
|
|
141
|
+
meta: { permissionState: { status: "granted", reason: "role" } },
|
|
142
|
+
});
|
|
165
143
|
expect(await handleRequest(rpcReq, config)).toEqual({
|
|
166
144
|
id: "123",
|
|
167
145
|
jsonrpc: "2.0",
|
|
@@ -177,19 +155,13 @@ test("when a role based permission has already been granted by the main runtime"
|
|
|
177
155
|
test("when there is an unexpected object thrown in the custom function", async () => {
|
|
178
156
|
const config = {
|
|
179
157
|
functions: {
|
|
180
|
-
createPost: async (ctx, inputs
|
|
181
|
-
api.permissions.allow();
|
|
158
|
+
createPost: async (ctx, inputs) => {
|
|
182
159
|
throw { err: "oopsie daisy" };
|
|
183
160
|
},
|
|
184
161
|
},
|
|
185
162
|
actionTypes: {
|
|
186
163
|
createPost: PROTO_ACTION_TYPES.CREATE,
|
|
187
164
|
},
|
|
188
|
-
createFunctionAPI: ({ db }) => {
|
|
189
|
-
return {
|
|
190
|
-
permissions: new Permissions(),
|
|
191
|
-
};
|
|
192
|
-
},
|
|
193
165
|
createContextAPI: () => {},
|
|
194
166
|
};
|
|
195
167
|
|
|
@@ -238,47 +210,44 @@ describe("ModelAPI error handling", () => {
|
|
|
238
210
|
INSERT INTO author (id, name) VALUES ('adam', 'adam bull')
|
|
239
211
|
`.execute(db);
|
|
240
212
|
|
|
213
|
+
const models = {
|
|
214
|
+
post: new ModelAPI(
|
|
215
|
+
"post",
|
|
216
|
+
() => {
|
|
217
|
+
return {
|
|
218
|
+
id: KSUID.randomSync().string,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
post: {
|
|
223
|
+
author: {
|
|
224
|
+
relationshipType: "belongsTo",
|
|
225
|
+
foreignKey: "author_id",
|
|
226
|
+
referencesTable: "person",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
};
|
|
232
|
+
|
|
241
233
|
functionConfig = {
|
|
242
|
-
|
|
234
|
+
permissionFns: {},
|
|
243
235
|
functions: {
|
|
244
|
-
createPost: async (ctx, inputs
|
|
245
|
-
|
|
236
|
+
createPost: async (ctx, inputs) => {
|
|
237
|
+
new Permissions().allow();
|
|
246
238
|
|
|
247
|
-
const post = await
|
|
239
|
+
const post = await models.post.create(inputs);
|
|
248
240
|
|
|
249
241
|
return post;
|
|
250
242
|
},
|
|
251
|
-
deletePost: async (ctx, inputs
|
|
252
|
-
|
|
243
|
+
deletePost: async (ctx, inputs) => {
|
|
244
|
+
new Permissions().allow();
|
|
253
245
|
|
|
254
|
-
const deleted = await
|
|
246
|
+
const deleted = await models.post.delete(inputs);
|
|
255
247
|
|
|
256
248
|
return deleted;
|
|
257
249
|
},
|
|
258
250
|
},
|
|
259
|
-
createFunctionAPI: ({ db }) => ({
|
|
260
|
-
permissions: new Permissions(),
|
|
261
|
-
models: {
|
|
262
|
-
post: new ModelAPI(
|
|
263
|
-
"post",
|
|
264
|
-
() => {
|
|
265
|
-
return {
|
|
266
|
-
id: KSUID.randomSync().string,
|
|
267
|
-
};
|
|
268
|
-
},
|
|
269
|
-
db,
|
|
270
|
-
{
|
|
271
|
-
post: {
|
|
272
|
-
author: {
|
|
273
|
-
relationshipType: "belongsTo",
|
|
274
|
-
foreignKey: "author_id",
|
|
275
|
-
referencesTable: "person",
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
}
|
|
279
|
-
),
|
|
280
|
-
},
|
|
281
|
-
}),
|
|
282
251
|
createContextAPI: () => ({}),
|
|
283
252
|
};
|
|
284
253
|
});
|
package/src/permissions.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require("async_hooks");
|
|
2
|
+
|
|
1
3
|
class PermissionError extends Error {}
|
|
2
4
|
|
|
3
5
|
const PERMISSION_STATE = {
|
|
@@ -6,45 +8,34 @@ const PERMISSION_STATE = {
|
|
|
6
8
|
UNPERMITTED: "unpermitted",
|
|
7
9
|
};
|
|
8
10
|
|
|
9
|
-
const
|
|
10
|
-
status: "unknown",
|
|
11
|
-
};
|
|
11
|
+
const permissionsApiInstance = new AsyncLocalStorage();
|
|
12
12
|
|
|
13
13
|
class Permissions {
|
|
14
|
-
// The permissionState here is the prior state passed in from the Go runtime
|
|
15
14
|
// The Go runtime performs role based permission rule checks prior to calling the functions
|
|
16
15
|
// runtime, so the status could already be granted. If already granted, then we need to inherit that permission state as the state is later used to decide whether to run in process permission checks
|
|
17
16
|
// TLDR if a role based permission is relevant and it is granted, then it is effectively the same as the end user calling api.permissions.allow() explicitly in terms of behaviour.
|
|
18
|
-
constructor(permissionState = defaultState) {
|
|
19
|
-
this.state = {
|
|
20
|
-
// permitted starts off as null to indicate that the end user
|
|
21
|
-
// hasn't explicitly marked a function execution as permitted yet
|
|
22
|
-
permitted:
|
|
23
|
-
permissionState !== null && permissionState.status === "granted"
|
|
24
|
-
? true
|
|
25
|
-
: null,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
17
|
|
|
29
18
|
allow() {
|
|
30
|
-
|
|
19
|
+
const store = (permissionsApiInstance.getStore().permitted = true);
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
deny() {
|
|
34
23
|
// if a user is explicitly calling deny() then we want to throw an error
|
|
35
24
|
// so that any further execution of the custom function stops abruptly
|
|
36
|
-
|
|
25
|
+
permissionsApiInstance.getStore().permitted = false;
|
|
37
26
|
|
|
38
27
|
throw new PermissionError();
|
|
39
28
|
}
|
|
40
29
|
|
|
41
30
|
getState() {
|
|
31
|
+
const permitted = permissionsApiInstance.getStore().permitted;
|
|
32
|
+
|
|
42
33
|
switch (true) {
|
|
43
|
-
case
|
|
34
|
+
case permitted === false:
|
|
44
35
|
return PERMISSION_STATE.UNPERMITTED;
|
|
45
|
-
case
|
|
36
|
+
case permitted === null:
|
|
46
37
|
return PERMISSION_STATE.UNKNOWN;
|
|
47
|
-
case
|
|
38
|
+
case permitted === true:
|
|
48
39
|
return PERMISSION_STATE.PERMITTED;
|
|
49
40
|
}
|
|
50
41
|
}
|
|
@@ -52,12 +43,12 @@ class Permissions {
|
|
|
52
43
|
|
|
53
44
|
const checkBuiltInPermissions = async ({
|
|
54
45
|
rows,
|
|
55
|
-
|
|
46
|
+
permissionFns,
|
|
56
47
|
ctx,
|
|
57
48
|
db,
|
|
58
49
|
functionName,
|
|
59
50
|
}) => {
|
|
60
|
-
for (const permissionFn of
|
|
51
|
+
for (const permissionFn of permissionFns) {
|
|
61
52
|
const result = await permissionFn(rows, ctx, db);
|
|
62
53
|
|
|
63
54
|
// if any of the permission functions return true,
|
|
@@ -70,6 +61,7 @@ const checkBuiltInPermissions = async ({
|
|
|
70
61
|
throw new PermissionError(`Not permitted to access ${functionName}`);
|
|
71
62
|
};
|
|
72
63
|
|
|
64
|
+
module.exports.permissionsApiInstance = permissionsApiInstance;
|
|
73
65
|
module.exports.checkBuiltInPermissions = checkBuiltInPermissions;
|
|
74
66
|
module.exports.PermissionError = PermissionError;
|
|
75
67
|
module.exports.PERMISSION_STATE = PERMISSION_STATE;
|