@teamkeel/functions-runtime 0.365.18 → 0.366.0-audit-logs0
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 +3 -2
- package/src/ModelAPI.js +1 -0
- package/src/auditing.js +119 -0
- package/src/auditing.test.js +362 -0
- package/src/database.js +5 -2
- package/src/handleRequest.test.js +0 -75
- package/src/handleSubscriber.js +1 -1
- package/src/tryExecuteFunction.js +6 -2
- package/src/tryExecuteJob.js +6 -3
- package/src/tryExecuteSubscriber.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamkeel/functions-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.366.0-audit-logs0",
|
|
4
4
|
"description": "Internal package used by @teamkeel/sdk",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"json-rpc-2.0": "^1.4.1",
|
|
29
29
|
"ksuid": "^3.0.0",
|
|
30
30
|
"kysely": "^0.23.4",
|
|
31
|
-
"pg": "^8.8.0"
|
|
31
|
+
"pg": "^8.8.0",
|
|
32
|
+
"traceparent": "^1.0.0"
|
|
32
33
|
}
|
|
33
34
|
}
|
package/src/ModelAPI.js
CHANGED
package/src/auditing.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require("async_hooks");
|
|
2
|
+
const TraceParent = require("traceparent");
|
|
3
|
+
const { sql, SelectionNode } = require("kysely");
|
|
4
|
+
|
|
5
|
+
const auditContextStorage = new AsyncLocalStorage();
|
|
6
|
+
|
|
7
|
+
// withAuditContext creates the audit context from the runtime request body
|
|
8
|
+
// and sets it to in AsyncLocalStorage so that this data is available to the
|
|
9
|
+
// ModelAPI during the execution of actions, jobs and subscribers.
|
|
10
|
+
async function withAuditContext(request, cb) {
|
|
11
|
+
let audit = {};
|
|
12
|
+
|
|
13
|
+
if (request.meta?.identity) {
|
|
14
|
+
audit.identityId = request.meta.identity.id;
|
|
15
|
+
}
|
|
16
|
+
if (request.meta?.tracing?.traceparent) {
|
|
17
|
+
audit.traceId = TraceParent.fromString(
|
|
18
|
+
request.meta.tracing.traceparent
|
|
19
|
+
)?.traceId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return await auditContextStorage.run(audit, () => {
|
|
23
|
+
return cb();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// getAuditContext retrieves the audit context from AsyncLocalStorage.
|
|
28
|
+
function getAuditContext() {
|
|
29
|
+
let auditStore = auditContextStorage.getStore();
|
|
30
|
+
return {
|
|
31
|
+
identityId: auditStore?.identityId,
|
|
32
|
+
traceId: auditStore?.traceId,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// AuditContextPlugin is a Kysely plugin which ensures that the audit context data
|
|
37
|
+
// is written to Postgres configuration parameters in the same execution as a query.
|
|
38
|
+
// It does this by calling the set_identity_id() and set_trace_id() functions as a
|
|
39
|
+
// clause in the returning statement. It then subsequently drops these from the actual result.
|
|
40
|
+
// This ensures that these parameters are set when the tables' AFTER trigger function executes.
|
|
41
|
+
class AuditContextPlugin {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.identityIdAlias = "__keel_identity_id";
|
|
44
|
+
this.traceIdAlias = "__keel_trace_id";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#setIdentityClause(value) {
|
|
48
|
+
return `set_identity_id('${value}')`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#setTraceIdClause(value) {
|
|
52
|
+
return `set_trace_id('${value}')`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Appends set_identity_id() and set_trace_id() function calls to the returning statement
|
|
56
|
+
// of INSERT, UPDATE and DELETE operations.
|
|
57
|
+
transformQuery(args) {
|
|
58
|
+
switch (args.node.kind) {
|
|
59
|
+
case "InsertQueryNode":
|
|
60
|
+
case "UpdateQueryNode":
|
|
61
|
+
case "DeleteQueryNode":
|
|
62
|
+
const returning = {
|
|
63
|
+
kind: "ReturningNode",
|
|
64
|
+
selections: [],
|
|
65
|
+
};
|
|
66
|
+
if (args.node.returning) {
|
|
67
|
+
returning.selections.push(...args.node.returning.selections);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Retrieve the audit context from async storage.
|
|
71
|
+
const audit = getAuditContext();
|
|
72
|
+
|
|
73
|
+
if (audit.identityId) {
|
|
74
|
+
const rawNode = sql
|
|
75
|
+
.raw(
|
|
76
|
+
this.#setIdentityClause(audit.identityId, this.identityIdAlias)
|
|
77
|
+
)
|
|
78
|
+
.as(this.identityIdAlias)
|
|
79
|
+
.toOperationNode();
|
|
80
|
+
|
|
81
|
+
returning.selections.push(SelectionNode.create(rawNode));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (audit.traceId) {
|
|
85
|
+
const rawNode = sql
|
|
86
|
+
.raw(this.#setTraceIdClause(audit.traceId))
|
|
87
|
+
.as(this.traceIdAlias)
|
|
88
|
+
.toOperationNode();
|
|
89
|
+
|
|
90
|
+
returning.selections.push(SelectionNode.create(rawNode));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...args.node,
|
|
95
|
+
returning: returning,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...args.node,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Drops the set_identity_id() and set_trace_id() fields from the result.
|
|
105
|
+
transformResult(args) {
|
|
106
|
+
if (args.result?.rows) {
|
|
107
|
+
for (let i = 0; i < args.result.rows.length; i++) {
|
|
108
|
+
delete args.result.rows[i][this.identityIdAlias];
|
|
109
|
+
delete args.result.rows[i][this.traceIdAlias];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return args.result;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports.withAuditContext = withAuditContext;
|
|
118
|
+
module.exports.getAuditContext = getAuditContext;
|
|
119
|
+
module.exports.AuditContextPlugin = AuditContextPlugin;
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { test, expect, beforeEach } from "vitest";
|
|
2
|
+
const { ModelAPI } = require("./ModelAPI");
|
|
3
|
+
const { PROTO_ACTION_TYPES } = require("./consts");
|
|
4
|
+
const { sql } = require("kysely");
|
|
5
|
+
const { useDatabase, withDatabase } = require("./database");
|
|
6
|
+
const KSUID = require("ksuid");
|
|
7
|
+
const TraceParent = require("traceparent");
|
|
8
|
+
const { withAuditContext } = require("./auditing");
|
|
9
|
+
|
|
10
|
+
let personAPI;
|
|
11
|
+
const db = useDatabase();
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
await sql`
|
|
15
|
+
|
|
16
|
+
DROP TABLE IF EXISTS post;
|
|
17
|
+
DROP TABLE IF EXISTS person;
|
|
18
|
+
DROP TABLE IF EXISTS author;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE person(
|
|
21
|
+
id text PRIMARY KEY,
|
|
22
|
+
name text UNIQUE
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE OR REPLACE FUNCTION set_identity_id(id VARCHAR)
|
|
26
|
+
RETURNS TEXT AS $$
|
|
27
|
+
BEGIN
|
|
28
|
+
RETURN set_config('audit.identity_id', id, true);
|
|
29
|
+
END
|
|
30
|
+
$$ LANGUAGE plpgsql;
|
|
31
|
+
|
|
32
|
+
CREATE OR REPLACE FUNCTION set_trace_id(id VARCHAR)
|
|
33
|
+
RETURNS TEXT AS $$
|
|
34
|
+
BEGIN
|
|
35
|
+
RETURN set_config('audit.trace_id', id, true);
|
|
36
|
+
END
|
|
37
|
+
$$ LANGUAGE plpgsql;
|
|
38
|
+
`.execute(db);
|
|
39
|
+
|
|
40
|
+
personAPI = new ModelAPI("person", undefined, {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function identityIdFromConfigParam(database, nonLocal = true) {
|
|
44
|
+
const result =
|
|
45
|
+
await sql`SELECT NULLIF(current_setting('audit.identity_id', ${sql.literal(
|
|
46
|
+
nonLocal
|
|
47
|
+
)}), '') AS id`.execute(database);
|
|
48
|
+
return result.rows[0].id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function traceIdFromConfigParam(database, nonLocal = true) {
|
|
52
|
+
const result =
|
|
53
|
+
await sql`SELECT NULLIF(current_setting('audit.trace_id', ${sql.literal(
|
|
54
|
+
nonLocal
|
|
55
|
+
)}), '') AS id`.execute(database);
|
|
56
|
+
return result.rows[0].id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("auditing - capturing identity id in transaction", async () => {
|
|
60
|
+
const request = {
|
|
61
|
+
meta: {
|
|
62
|
+
identity: { id: KSUID.randomSync().string },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const identityId = request.meta.identity.id;
|
|
67
|
+
|
|
68
|
+
const row = await withDatabase(
|
|
69
|
+
db,
|
|
70
|
+
PROTO_ACTION_TYPES.CREATE, // CREATE will ensure a transaction is opened
|
|
71
|
+
async ({ transaction }) => {
|
|
72
|
+
const row = withAuditContext(request, async () => {
|
|
73
|
+
return await personAPI.create({
|
|
74
|
+
id: KSUID.randomSync().string,
|
|
75
|
+
name: "James",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
80
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
81
|
+
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(row.name).toEqual("James");
|
|
87
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
88
|
+
expect(await identityIdFromConfigParam(db, false)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("auditing - capturing tracing in transaction", async () => {
|
|
92
|
+
const request = {
|
|
93
|
+
meta: {
|
|
94
|
+
tracing: {
|
|
95
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const traceId = TraceParent.fromString(
|
|
101
|
+
request.meta.tracing.traceparent
|
|
102
|
+
).traceId;
|
|
103
|
+
|
|
104
|
+
const row = await withDatabase(
|
|
105
|
+
db,
|
|
106
|
+
PROTO_ACTION_TYPES.CREATE, // CREATE will ensure a transaction is opened
|
|
107
|
+
async ({ transaction }) => {
|
|
108
|
+
const row = withAuditContext(request, async () => {
|
|
109
|
+
return await personAPI.create({
|
|
110
|
+
id: KSUID.randomSync().string,
|
|
111
|
+
name: "Jim",
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
116
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
117
|
+
|
|
118
|
+
return row;
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(row.name).toEqual("Jim");
|
|
123
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
124
|
+
expect(await traceIdFromConfigParam(db, false)).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("auditing - capturing identity id without transaction", async () => {
|
|
128
|
+
const request = {
|
|
129
|
+
meta: {
|
|
130
|
+
identity: { id: KSUID.randomSync().string },
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const row = await withDatabase(
|
|
135
|
+
db,
|
|
136
|
+
PROTO_ACTION_TYPES.GET, // GET will _not_ open a transaction
|
|
137
|
+
async ({ sDb }) => {
|
|
138
|
+
const row = withAuditContext(request, async () => {
|
|
139
|
+
return await personAPI.create({
|
|
140
|
+
id: KSUID.randomSync().string,
|
|
141
|
+
name: "James",
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(await identityIdFromConfigParam(sDb)).toBeNull();
|
|
146
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
147
|
+
|
|
148
|
+
return row;
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(row.name).toEqual("James");
|
|
153
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
154
|
+
expect(await identityIdFromConfigParam(db, false)).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("auditing - capturing tracing without transaction", async () => {
|
|
158
|
+
const request = {
|
|
159
|
+
meta: {
|
|
160
|
+
tracing: {
|
|
161
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const row = await withDatabase(
|
|
167
|
+
db,
|
|
168
|
+
PROTO_ACTION_TYPES.GET, // GET will _not_ open a transaction
|
|
169
|
+
async ({ sDb }) => {
|
|
170
|
+
const row = withAuditContext(request, async () => {
|
|
171
|
+
return await personAPI.create({
|
|
172
|
+
id: KSUID.randomSync().string,
|
|
173
|
+
name: "Jim",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(await traceIdFromConfigParam(sDb)).toBeNull();
|
|
178
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
179
|
+
|
|
180
|
+
return row;
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(row.name).toEqual("Jim");
|
|
185
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
186
|
+
expect(await traceIdFromConfigParam(db, false)).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("auditing - no audit context", async () => {
|
|
190
|
+
const row = await withDatabase(
|
|
191
|
+
db,
|
|
192
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
193
|
+
async ({ transaction }) => {
|
|
194
|
+
const row = withAuditContext({}, async () => {
|
|
195
|
+
return await personAPI.create({
|
|
196
|
+
id: KSUID.randomSync().string,
|
|
197
|
+
name: "Jake",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(await identityIdFromConfigParam(transaction)).toBeNull();
|
|
202
|
+
expect(await identityIdFromConfigParam(db)).toBeNull();
|
|
203
|
+
expect(await traceIdFromConfigParam(transaction)).toBeNull();
|
|
204
|
+
expect(await traceIdFromConfigParam(db)).toBeNull();
|
|
205
|
+
return row;
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(KSUID.parse(row.id).string).toEqual(row.id);
|
|
210
|
+
expect(row.name).toEqual("Jake");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("auditing - ModelAPI.create", async () => {
|
|
214
|
+
const request = {
|
|
215
|
+
meta: {
|
|
216
|
+
identity: { id: KSUID.randomSync().string },
|
|
217
|
+
tracing: {
|
|
218
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const identityId = request.meta.identity.id;
|
|
224
|
+
const traceId = TraceParent.fromString(
|
|
225
|
+
request.meta.tracing.traceparent
|
|
226
|
+
).traceId;
|
|
227
|
+
|
|
228
|
+
const row = await withDatabase(
|
|
229
|
+
db,
|
|
230
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
231
|
+
async ({ transaction }) => {
|
|
232
|
+
const row = withAuditContext(request, async () => {
|
|
233
|
+
return await personAPI.create({
|
|
234
|
+
id: KSUID.randomSync().string,
|
|
235
|
+
name: "Jake",
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
240
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
241
|
+
|
|
242
|
+
return row;
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(row.name).toEqual("Jake");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("auditing - ModelAPI.update", async () => {
|
|
250
|
+
const request = {
|
|
251
|
+
meta: {
|
|
252
|
+
identity: { id: KSUID.randomSync().string },
|
|
253
|
+
tracing: {
|
|
254
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const identityId = request.meta.identity.id;
|
|
260
|
+
const traceId = TraceParent.fromString(
|
|
261
|
+
request.meta.tracing.traceparent
|
|
262
|
+
).traceId;
|
|
263
|
+
|
|
264
|
+
const created = await personAPI.create({
|
|
265
|
+
id: KSUID.randomSync().string,
|
|
266
|
+
name: "Jake",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const row = await withDatabase(
|
|
270
|
+
db,
|
|
271
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
272
|
+
async ({ transaction }) => {
|
|
273
|
+
const row = withAuditContext(request, async () => {
|
|
274
|
+
return await personAPI.update({ id: created.id }, { name: "Jim" });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
278
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
279
|
+
|
|
280
|
+
return row;
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(row.name).toEqual("Jim");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("auditing - ModelAPI.delete", async () => {
|
|
288
|
+
const request = {
|
|
289
|
+
meta: {
|
|
290
|
+
identity: { id: KSUID.randomSync().string },
|
|
291
|
+
tracing: {
|
|
292
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const identityId = request.meta.identity.id;
|
|
298
|
+
const traceId = TraceParent.fromString(
|
|
299
|
+
request.meta.tracing.traceparent
|
|
300
|
+
).traceId;
|
|
301
|
+
|
|
302
|
+
const created = await personAPI.create({
|
|
303
|
+
id: KSUID.randomSync().string,
|
|
304
|
+
name: "Jake",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const row = await withDatabase(
|
|
308
|
+
db,
|
|
309
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
310
|
+
async ({ transaction }) => {
|
|
311
|
+
const row = withAuditContext(request, async () => {
|
|
312
|
+
return await personAPI.delete({ id: created.id });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
316
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
317
|
+
|
|
318
|
+
return row;
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(row).toEqual(created.id);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("auditing - identity id and trace id fields dropped from result", async () => {
|
|
326
|
+
const request = {
|
|
327
|
+
meta: {
|
|
328
|
+
identity: { id: KSUID.randomSync().string },
|
|
329
|
+
tracing: {
|
|
330
|
+
traceparent: "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const identityId = request.meta.identity.id;
|
|
336
|
+
const traceId = TraceParent.fromString(
|
|
337
|
+
request.meta.tracing.traceparent
|
|
338
|
+
).traceId;
|
|
339
|
+
|
|
340
|
+
const row = await withDatabase(
|
|
341
|
+
db,
|
|
342
|
+
PROTO_ACTION_TYPES.CREATE,
|
|
343
|
+
async ({ transaction }) => {
|
|
344
|
+
const row = withAuditContext(request, async () => {
|
|
345
|
+
return await personAPI.create({
|
|
346
|
+
id: KSUID.randomSync().string,
|
|
347
|
+
name: "Jake",
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(await identityIdFromConfigParam(transaction)).toEqual(identityId);
|
|
352
|
+
expect(await traceIdFromConfigParam(transaction)).toEqual(traceId);
|
|
353
|
+
|
|
354
|
+
return row;
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(row.name).toEqual("Jake");
|
|
359
|
+
expect(row.keelIdentityId).toBeUndefined();
|
|
360
|
+
expect(row.keelTraceId).toBeUndefined();
|
|
361
|
+
expect(Object.keys(row).length).toEqual(2);
|
|
362
|
+
});
|
package/src/database.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { Kysely, PostgresDialect, CamelCasePlugin } = require("kysely");
|
|
2
2
|
const { AsyncLocalStorage } = require("async_hooks");
|
|
3
|
+
const { AuditContextPlugin } = require("./auditing");
|
|
3
4
|
const pg = require("pg");
|
|
4
5
|
const { PROTO_ACTION_TYPES } = require("./consts");
|
|
5
6
|
const { withSpan } = require("./tracing");
|
|
@@ -15,6 +16,7 @@ async function withDatabase(db, actionType, cb) {
|
|
|
15
16
|
let requiresTransaction = true;
|
|
16
17
|
|
|
17
18
|
switch (actionType) {
|
|
19
|
+
case PROTO_ACTION_TYPES.SUBSCRIBER:
|
|
18
20
|
case PROTO_ACTION_TYPES.JOB:
|
|
19
21
|
case PROTO_ACTION_TYPES.GET:
|
|
20
22
|
case PROTO_ACTION_TYPES.LIST:
|
|
@@ -78,6 +80,8 @@ function getDatabaseClient() {
|
|
|
78
80
|
db = new Kysely({
|
|
79
81
|
dialect: getDialect(),
|
|
80
82
|
plugins: [
|
|
83
|
+
// ensures that the audit context data is written to Postgres configuration parameters
|
|
84
|
+
new AuditContextPlugin(),
|
|
81
85
|
// allows users to query using camelCased versions of the database column names, which
|
|
82
86
|
// should match the names we use in our schema.
|
|
83
87
|
// https://kysely-org.github.io/kysely/classes/CamelCasePlugin.html
|
|
@@ -119,14 +123,13 @@ class InstrumentedClient extends pg.Client {
|
|
|
119
123
|
const sql = args[0];
|
|
120
124
|
|
|
121
125
|
let sqlAttribute = false;
|
|
122
|
-
|
|
123
126
|
let spanName = txStatements[sql.toLowerCase()];
|
|
124
127
|
if (!spanName) {
|
|
125
128
|
spanName = "Database Query";
|
|
126
129
|
sqlAttribute = true;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
return withSpan(spanName, function (span) {
|
|
132
|
+
return await withSpan(spanName, function (span) {
|
|
130
133
|
if (sqlAttribute) {
|
|
131
134
|
span.setAttribute("sql", args[0]);
|
|
132
135
|
}
|
|
@@ -282,79 +282,4 @@ describe("ModelAPI error handling", () => {
|
|
|
282
282
|
},
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
|
-
|
|
286
|
-
test("when there is a uniqueness constraint error", async () => {
|
|
287
|
-
await sql`
|
|
288
|
-
|
|
289
|
-
INSERT INTO post (id, title, author_id) values(${
|
|
290
|
-
KSUID.randomSync().string
|
|
291
|
-
}, 'hello', 'adam')
|
|
292
|
-
`.execute(db);
|
|
293
|
-
|
|
294
|
-
const rpcReq = createJSONRPCRequest("123", "createPost", {
|
|
295
|
-
title: "hello",
|
|
296
|
-
author_id: "something",
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
expect(await handleRequest(rpcReq, functionConfig)).toEqual({
|
|
300
|
-
id: "123",
|
|
301
|
-
jsonrpc: "2.0",
|
|
302
|
-
error: {
|
|
303
|
-
code: RuntimeErrors.UniqueConstraintError,
|
|
304
|
-
message:
|
|
305
|
-
'duplicate key value violates unique constraint "post_title_key"',
|
|
306
|
-
data: {
|
|
307
|
-
code: "23505",
|
|
308
|
-
column: "title",
|
|
309
|
-
detail: "Key (title)=(hello) already exists.",
|
|
310
|
-
table: "post",
|
|
311
|
-
value: "hello",
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("when there is a null value in a foreign key column", async () => {
|
|
318
|
-
const rpcReq = createJSONRPCRequest("123", "createPost", { title: "123" });
|
|
319
|
-
|
|
320
|
-
expect(await handleRequest(rpcReq, functionConfig)).toEqual({
|
|
321
|
-
id: "123",
|
|
322
|
-
jsonrpc: "2.0",
|
|
323
|
-
error: {
|
|
324
|
-
code: RuntimeErrors.NotNullConstraintError,
|
|
325
|
-
message:
|
|
326
|
-
'null value in column "author_id" violates not-null constraint',
|
|
327
|
-
data: {
|
|
328
|
-
code: "23502",
|
|
329
|
-
column: "author_id",
|
|
330
|
-
detail: expect.stringContaining("Failing row contains"),
|
|
331
|
-
table: "post",
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test("when there is a foreign key constraint violation", async () => {
|
|
338
|
-
const rpcReq2 = createJSONRPCRequest("123", "createPost", {
|
|
339
|
-
title: "123",
|
|
340
|
-
author_id: "fake",
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
expect(await handleRequest(rpcReq2, functionConfig)).toEqual({
|
|
344
|
-
id: "123",
|
|
345
|
-
jsonrpc: "2.0",
|
|
346
|
-
error: {
|
|
347
|
-
code: RuntimeErrors.ForeignKeyConstraintError,
|
|
348
|
-
message:
|
|
349
|
-
'insert or update on table "post" violates foreign key constraint "post_author_id_fkey"',
|
|
350
|
-
data: {
|
|
351
|
-
code: "23503",
|
|
352
|
-
column: "author_id",
|
|
353
|
-
detail: 'Key (author_id)=(fake) is not present in table "author".',
|
|
354
|
-
table: "post",
|
|
355
|
-
value: "fake",
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
285
|
});
|
package/src/handleSubscriber.js
CHANGED
|
@@ -49,7 +49,7 @@ async function handleSubscriber(request, config) {
|
|
|
49
49
|
const subscriberFunction = subscribers[request.method];
|
|
50
50
|
const actionType = PROTO_ACTION_TYPES.SUBSCRIBER;
|
|
51
51
|
|
|
52
|
-
await tryExecuteSubscriber({ db, actionType }, async () => {
|
|
52
|
+
await tryExecuteSubscriber({ request, db, actionType }, async () => {
|
|
53
53
|
// Return the subscriber function to the containing tryExecuteSubscriber block
|
|
54
54
|
return subscriberFunction(ctx, request.params);
|
|
55
55
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { withDatabase } = require("./database");
|
|
2
|
+
const { withAuditContext } = require("./auditing");
|
|
2
3
|
const {
|
|
3
4
|
withPermissions,
|
|
4
5
|
PERMISSION_STATE,
|
|
@@ -10,12 +11,15 @@ const { PROTO_ACTION_TYPES } = require("./consts");
|
|
|
10
11
|
// tryExecuteFunction will create a new database transaction around a function call
|
|
11
12
|
// and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit.
|
|
12
13
|
function tryExecuteFunction(
|
|
13
|
-
{ db, permitted, permissionFns, actionType,
|
|
14
|
+
{ request, db, permitted, permissionFns, actionType, ctx },
|
|
14
15
|
cb
|
|
15
16
|
) {
|
|
16
17
|
return withPermissions(permitted, async ({ getPermissionState }) => {
|
|
17
18
|
return withDatabase(db, actionType, async ({ transaction }) => {
|
|
18
|
-
const fnResult = await
|
|
19
|
+
const fnResult = await withAuditContext(request, async () => {
|
|
20
|
+
return cb();
|
|
21
|
+
});
|
|
22
|
+
|
|
19
23
|
// api.permissions maintains an internal state of whether the current function 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).
|
|
20
24
|
// 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
|
|
21
25
|
// and therefore we default to checking the permissions defined in the schema automatically.
|
package/src/tryExecuteJob.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
const { withDatabase } = require("./database");
|
|
2
|
+
const { withAuditContext } = require("./auditing");
|
|
2
3
|
const { withPermissions, PERMISSION_STATE } = require("./permissions");
|
|
3
|
-
|
|
4
4
|
const { PermissionError } = require("./errors");
|
|
5
5
|
|
|
6
6
|
// tryExecuteJob will create a new database transaction around a function call
|
|
7
7
|
// and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit.
|
|
8
8
|
function tryExecuteJob({ db, permitted, actionType, request }, cb) {
|
|
9
9
|
return withPermissions(permitted, async ({ getPermissionState }) => {
|
|
10
|
-
return withDatabase(db, actionType, async (
|
|
11
|
-
await
|
|
10
|
+
return withDatabase(db, actionType, async () => {
|
|
11
|
+
await withAuditContext(request, async () => {
|
|
12
|
+
return cb();
|
|
13
|
+
});
|
|
14
|
+
|
|
12
15
|
// 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).
|
|
13
16
|
// 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
|
|
14
17
|
// and therefore we default to checking the permissions defined in the schema automatically.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const { withDatabase } = require("./database");
|
|
2
|
+
const { withAuditContext } = require("./auditing");
|
|
2
3
|
|
|
3
4
|
// tryExecuteSubscriber will create a new database connection and execute the function call.
|
|
4
|
-
function tryExecuteSubscriber({ db, actionType }, cb) {
|
|
5
|
+
function tryExecuteSubscriber({ request, db, actionType }, cb) {
|
|
5
6
|
return withDatabase(db, actionType, async () => {
|
|
6
|
-
await
|
|
7
|
+
await withAuditContext(request, async () => {
|
|
8
|
+
return cb();
|
|
9
|
+
});
|
|
7
10
|
});
|
|
8
11
|
}
|
|
9
12
|
|