directus 9.15.1 → 9.17.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/dist/__utils__/items-utils.d.ts +2 -0
- package/dist/__utils__/items-utils.js +36 -0
- package/dist/__utils__/schemas.d.ts +13 -0
- package/dist/__utils__/schemas.js +304 -0
- package/dist/__utils__/snapshots.d.ts +5 -0
- package/dist/__utils__/snapshots.js +897 -0
- package/dist/app.js +8 -7
- package/dist/auth/drivers/ldap.js +1 -0
- package/dist/auth/drivers/local.js +6 -0
- package/dist/auth/drivers/oauth2.js +1 -0
- package/dist/auth/drivers/openid.js +1 -0
- package/dist/cli/index.test.d.ts +1 -0
- package/dist/cli/index.test.js +58 -0
- package/dist/cli/utils/create-env/env-stub.liquid +6 -2
- package/dist/controllers/activity.js +1 -0
- package/dist/controllers/assets.js +20 -16
- package/dist/controllers/auth.js +6 -9
- package/dist/controllers/files.test.d.ts +1 -0
- package/dist/controllers/files.test.js +49 -0
- package/dist/controllers/server.js +0 -1
- package/dist/database/migrations/20220826A-add-origin-to-accountability.d.ts +3 -0
- package/dist/database/migrations/20220826A-add-origin-to-accountability.js +21 -0
- package/dist/database/migrations/run.test.d.ts +1 -0
- package/dist/database/migrations/run.test.js +92 -0
- package/dist/database/system-data/fields/activity.yaml +6 -0
- package/dist/database/system-data/fields/sessions.yaml +2 -0
- package/dist/env.js +15 -0
- package/dist/env.test.d.ts +8 -0
- package/dist/env.test.js +39 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +16 -3
- package/dist/flows.js +28 -17
- package/dist/mailer.js +1 -0
- package/dist/middleware/authenticate.d.ts +1 -1
- package/dist/middleware/authenticate.js +1 -0
- package/dist/middleware/authenticate.test.d.ts +1 -0
- package/dist/middleware/authenticate.test.js +214 -0
- package/dist/middleware/extract-token.test.d.ts +1 -0
- package/dist/middleware/extract-token.test.js +60 -0
- package/dist/middleware/validate-batch.d.ts +1 -2
- package/dist/middleware/validate-batch.js +10 -13
- package/dist/middleware/validate-batch.test.d.ts +1 -0
- package/dist/middleware/validate-batch.test.js +82 -0
- package/dist/operations/exec/index.d.ts +5 -0
- package/dist/operations/exec/index.js +26 -0
- package/dist/operations/exec/index.test.d.ts +1 -0
- package/dist/operations/exec/index.test.js +95 -0
- package/dist/operations/notification/index.js +9 -6
- package/dist/operations/request/index.js +22 -3
- package/dist/operations/transform/index.d.ts +1 -1
- package/dist/operations/transform/index.js +1 -1
- package/dist/services/authentication.js +13 -3
- package/dist/services/files.js +3 -2
- package/dist/services/files.test.d.ts +1 -0
- package/dist/services/files.test.js +53 -0
- package/dist/services/flows.js +4 -0
- package/dist/services/graphql/index.d.ts +2 -2
- package/dist/services/graphql/index.js +78 -75
- package/dist/services/items.js +98 -42
- package/dist/services/items.test.d.ts +1 -0
- package/dist/services/items.test.js +765 -0
- package/dist/services/payload.d.ts +7 -4
- package/dist/services/payload.js +63 -12
- package/dist/services/payload.test.d.ts +1 -0
- package/dist/services/payload.test.js +94 -0
- package/dist/services/server.js +10 -7
- package/dist/services/shares.js +2 -1
- package/dist/services/specifications.test.d.ts +1 -0
- package/dist/services/specifications.test.js +96 -0
- package/dist/types/items.d.ts +11 -0
- package/dist/utils/apply-query.js +7 -3
- package/dist/utils/apply-snapshot.js +15 -0
- package/dist/utils/apply-snapshot.test.d.ts +1 -0
- package/dist/utils/apply-snapshot.test.js +305 -0
- package/dist/utils/async-handler.d.ts +2 -6
- package/dist/utils/async-handler.js +1 -13
- package/dist/utils/async-handler.test.d.ts +1 -0
- package/dist/utils/async-handler.test.js +18 -0
- package/dist/utils/calculate-field-depth.test.d.ts +1 -0
- package/dist/utils/calculate-field-depth.test.js +76 -0
- package/dist/utils/filter-items.test.d.ts +1 -0
- package/dist/utils/filter-items.test.js +60 -0
- package/dist/utils/get-cache-key.test.d.ts +1 -0
- package/dist/utils/get-cache-key.test.js +53 -0
- package/dist/utils/get-column-path.test.d.ts +1 -0
- package/dist/utils/get-column-path.test.js +136 -0
- package/dist/utils/get-config-from-env.test.d.ts +1 -0
- package/dist/utils/get-config-from-env.test.js +19 -0
- package/dist/utils/get-graphql-type.d.ts +1 -1
- package/dist/utils/get-graphql-type.js +4 -1
- package/dist/utils/get-os-info.d.ts +9 -0
- package/dist/utils/get-os-info.js +47 -0
- package/dist/utils/get-relation-info.test.d.ts +1 -0
- package/dist/utils/get-relation-info.test.js +88 -0
- package/dist/utils/get-relation-type.test.d.ts +1 -0
- package/dist/utils/get-relation-type.test.js +69 -0
- package/dist/utils/get-string-byte-size.test.d.ts +1 -0
- package/dist/utils/get-string-byte-size.test.js +8 -0
- package/dist/utils/is-directus-jwt.test.d.ts +1 -0
- package/dist/utils/is-directus-jwt.test.js +26 -0
- package/dist/utils/jwt.test.d.ts +1 -0
- package/dist/utils/jwt.test.js +36 -0
- package/dist/utils/merge-permissions.test.d.ts +1 -0
- package/dist/utils/merge-permissions.test.js +80 -0
- package/dist/utils/validate-keys.test.d.ts +1 -0
- package/dist/utils/validate-keys.test.js +97 -0
- package/package.json +14 -12
package/dist/extensions.js
CHANGED
|
@@ -78,19 +78,27 @@ class ExtensionManager {
|
|
|
78
78
|
this.reloadQueue = new job_queue_1.JobQueue();
|
|
79
79
|
}
|
|
80
80
|
async initialize(options = {}) {
|
|
81
|
+
const prevOptions = this.options;
|
|
81
82
|
this.options = {
|
|
82
83
|
...defaultOptions,
|
|
83
84
|
...options,
|
|
84
85
|
};
|
|
85
|
-
this.
|
|
86
|
+
if (!prevOptions.watch && this.options.watch) {
|
|
87
|
+
this.initializeWatcher();
|
|
88
|
+
}
|
|
89
|
+
else if (prevOptions.watch && !this.options.watch) {
|
|
90
|
+
await this.closeWatcher();
|
|
91
|
+
}
|
|
86
92
|
if (!this.isLoaded) {
|
|
87
93
|
await this.load();
|
|
88
|
-
this.updateWatchedExtensions(this.extensions);
|
|
89
94
|
const loadedExtensions = this.getExtensionsList();
|
|
90
95
|
if (loadedExtensions.length > 0) {
|
|
91
96
|
logger_1.default.info(`Loaded extensions: ${loadedExtensions.join(', ')}`);
|
|
92
97
|
}
|
|
93
98
|
}
|
|
99
|
+
if (!prevOptions.watch && this.options.watch) {
|
|
100
|
+
this.updateWatchedExtensions(this.extensions);
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
reload() {
|
|
96
104
|
this.reloadQueue.enqueue(async () => {
|
|
@@ -158,7 +166,7 @@ class ExtensionManager {
|
|
|
158
166
|
this.isLoaded = false;
|
|
159
167
|
}
|
|
160
168
|
initializeWatcher() {
|
|
161
|
-
if (
|
|
169
|
+
if (!this.watcher) {
|
|
162
170
|
logger_1.default.info('Watching extensions for changes...');
|
|
163
171
|
const localExtensionPaths = (env_1.default.SERVE_APP ? constants_1.EXTENSION_TYPES : constants_1.API_OR_HYBRID_EXTENSION_TYPES).flatMap((type) => {
|
|
164
172
|
const typeDir = path_1.default.posix.join(path_1.default.relative('.', env_1.default.EXTENSIONS_PATH).split(path_1.default.sep).join(path_1.default.posix.sep), (0, utils_1.pluralize)(type));
|
|
@@ -175,6 +183,11 @@ class ExtensionManager {
|
|
|
175
183
|
.on('unlink', () => this.reload());
|
|
176
184
|
}
|
|
177
185
|
}
|
|
186
|
+
async closeWatcher() {
|
|
187
|
+
if (this.watcher) {
|
|
188
|
+
await this.watcher.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
178
191
|
updateWatchedExtensions(added, removed = []) {
|
|
179
192
|
if (this.watcher) {
|
|
180
193
|
const toPackageExtensionPaths = (extensions) => extensions
|
package/dist/flows.js
CHANGED
|
@@ -119,28 +119,33 @@ class FlowManager {
|
|
|
119
119
|
return handler(data, context);
|
|
120
120
|
}
|
|
121
121
|
async load() {
|
|
122
|
-
var _a, _b, _c, _d
|
|
122
|
+
var _a, _b, _c, _d;
|
|
123
123
|
const flowsService = new services_1.FlowsService({ knex: (0, database_1.default)(), schema: await (0, get_schema_1.getSchema)() });
|
|
124
124
|
const flows = await flowsService.readByQuery({
|
|
125
125
|
filter: { status: { _eq: 'active' } },
|
|
126
126
|
fields: ['*', 'operations.*'],
|
|
127
|
+
limit: -1,
|
|
127
128
|
});
|
|
128
129
|
const flowTrees = flows.map((flow) => (0, construct_flow_tree_1.constructFlowTree)(flow));
|
|
129
130
|
for (const flow of flowTrees) {
|
|
130
131
|
if (flow.trigger === 'event') {
|
|
131
|
-
const events = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
const events = ((_a = flow.options) === null || _a === void 0 ? void 0 : _a.scope)
|
|
133
|
+
? (0, utils_1.toArray)(flow.options.scope)
|
|
134
|
+
.map((scope) => {
|
|
135
|
+
var _a, _b, _c;
|
|
136
|
+
if (['items.create', 'items.update', 'items.delete'].includes(scope)) {
|
|
137
|
+
return ((_c = (_b = (_a = flow.options) === null || _a === void 0 ? void 0 : _a.collections) === null || _b === void 0 ? void 0 : _b.map((collection) => {
|
|
138
|
+
if (collection.startsWith('directus_')) {
|
|
139
|
+
const action = scope.split('.')[1];
|
|
140
|
+
return collection.substring(9) + '.' + action;
|
|
141
|
+
}
|
|
142
|
+
return `${collection}.${scope}`;
|
|
143
|
+
})) !== null && _c !== void 0 ? _c : []);
|
|
144
|
+
}
|
|
145
|
+
return scope;
|
|
146
|
+
})
|
|
147
|
+
.flat()
|
|
148
|
+
: [];
|
|
144
149
|
if (flow.options.type === 'filter') {
|
|
145
150
|
const handler = (payload, meta, context) => this.executeFlow(flow, { payload, ...meta }, {
|
|
146
151
|
accountability: context.accountability,
|
|
@@ -195,9 +200,9 @@ class FlowManager {
|
|
|
195
200
|
return this.executeFlow(flow, data, context);
|
|
196
201
|
}
|
|
197
202
|
};
|
|
198
|
-
const method = (
|
|
203
|
+
const method = (_c = (_b = flow.options) === null || _b === void 0 ? void 0 : _b.method) !== null && _c !== void 0 ? _c : 'GET';
|
|
199
204
|
// Default return to $last for webhooks
|
|
200
|
-
flow.options.return = (
|
|
205
|
+
flow.options.return = (_d = flow.options.return) !== null && _d !== void 0 ? _d : '$last';
|
|
201
206
|
this.webhookFlowHandlers[`${method}-${flow.id}`] = handler;
|
|
202
207
|
}
|
|
203
208
|
else if (flow.trigger === 'manual') {
|
|
@@ -253,7 +258,7 @@ class FlowManager {
|
|
|
253
258
|
this.isLoaded = false;
|
|
254
259
|
}
|
|
255
260
|
async executeFlow(flow, data = null, context = {}) {
|
|
256
|
-
var _a, _b, _c, _d, _e, _f;
|
|
261
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
257
262
|
const database = (_a = context.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
|
|
258
263
|
const schema = (_b = context.schema) !== null && _b !== void 0 ? _b : (await (0, get_schema_1.getSchema)({ database }));
|
|
259
264
|
const keyedData = {
|
|
@@ -262,11 +267,13 @@ class FlowManager {
|
|
|
262
267
|
[ACCOUNTABILITY_KEY]: (_c = context === null || context === void 0 ? void 0 : context.accountability) !== null && _c !== void 0 ? _c : null,
|
|
263
268
|
};
|
|
264
269
|
let nextOperation = flow.operation;
|
|
270
|
+
let lastOperationStatus = 'unknown';
|
|
265
271
|
const steps = [];
|
|
266
272
|
while (nextOperation !== null) {
|
|
267
273
|
const { successor, data, status, options } = await this.executeOperation(nextOperation, keyedData, context);
|
|
268
274
|
keyedData[nextOperation.key] = data;
|
|
269
275
|
keyedData[LAST_KEY] = data;
|
|
276
|
+
lastOperationStatus = status;
|
|
270
277
|
steps.push({ operation: nextOperation.id, key: nextOperation.key, status, options });
|
|
271
278
|
nextOperation = successor;
|
|
272
279
|
}
|
|
@@ -282,6 +289,7 @@ class FlowManager {
|
|
|
282
289
|
collection: 'directus_flows',
|
|
283
290
|
ip: (_e = accountability === null || accountability === void 0 ? void 0 : accountability.ip) !== null && _e !== void 0 ? _e : null,
|
|
284
291
|
user_agent: (_f = accountability === null || accountability === void 0 ? void 0 : accountability.userAgent) !== null && _f !== void 0 ? _f : null,
|
|
292
|
+
origin: (_g = accountability === null || accountability === void 0 ? void 0 : accountability.origin) !== null && _g !== void 0 ? _g : null,
|
|
285
293
|
item: flow.id,
|
|
286
294
|
});
|
|
287
295
|
if (flow.accountability === 'all') {
|
|
@@ -300,6 +308,9 @@ class FlowManager {
|
|
|
300
308
|
});
|
|
301
309
|
}
|
|
302
310
|
}
|
|
311
|
+
if (flow.trigger === 'event' && flow.options.type === 'filter' && lastOperationStatus === 'reject') {
|
|
312
|
+
throw keyedData[LAST_KEY];
|
|
313
|
+
}
|
|
303
314
|
if (flow.options.return === '$all') {
|
|
304
315
|
return keyedData;
|
|
305
316
|
}
|
package/dist/mailer.js
CHANGED
|
@@ -37,6 +37,7 @@ function getMailer() {
|
|
|
37
37
|
}
|
|
38
38
|
const tls = (0, get_config_from_env_1.getConfigFromEnv)('EMAIL_SMTP_TLS_');
|
|
39
39
|
transporter = nodemailer_1.default.createTransport({
|
|
40
|
+
name: env_1.default.EMAIL_SMTP_NAME,
|
|
40
41
|
pool: env_1.default.EMAIL_SMTP_POOL,
|
|
41
42
|
host: env_1.default.EMAIL_SMTP_HOST,
|
|
42
43
|
port: env_1.default.EMAIL_SMTP_PORT,
|
|
@@ -3,5 +3,5 @@ import { NextFunction, Request, Response } from 'express';
|
|
|
3
3
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
4
4
|
*/
|
|
5
5
|
export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
6
|
-
declare const _default:
|
|
6
|
+
declare const _default: (req: Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction) => Promise<void>;
|
|
7
7
|
export default _default;
|
|
@@ -24,6 +24,7 @@ const handler = async (req, res, next) => {
|
|
|
24
24
|
app: false,
|
|
25
25
|
ip: (0, get_ip_from_req_1.getIPFromReq)(req),
|
|
26
26
|
userAgent: req.get('user-agent'),
|
|
27
|
+
origin: req.get('origin'),
|
|
27
28
|
};
|
|
28
29
|
const database = (0, database_1.default)();
|
|
29
30
|
const customAccountability = await emitter_1.default.emitFilter('authenticate', defaultAccountability, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../../src/types/express.d.ts';
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const database_1 = __importDefault(require("../database"));
|
|
9
|
+
const emitter_1 = __importDefault(require("../emitter"));
|
|
10
|
+
const env_1 = __importDefault(require("../env"));
|
|
11
|
+
const exceptions_1 = require("../exceptions");
|
|
12
|
+
const authenticate_1 = require("./authenticate");
|
|
13
|
+
require("../../src/types/express.d.ts");
|
|
14
|
+
jest.mock('../../src/database');
|
|
15
|
+
jest.mock('../../src/env', () => ({
|
|
16
|
+
SECRET: 'test',
|
|
17
|
+
}));
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.resetAllMocks();
|
|
20
|
+
});
|
|
21
|
+
test('Short-circuits when authenticate filter is used', async () => {
|
|
22
|
+
const req = {
|
|
23
|
+
ip: '127.0.0.1',
|
|
24
|
+
get: jest.fn(),
|
|
25
|
+
};
|
|
26
|
+
const res = {};
|
|
27
|
+
const next = jest.fn();
|
|
28
|
+
const customAccountability = { admin: true };
|
|
29
|
+
jest.spyOn(emitter_1.default, 'emitFilter').mockResolvedValue(customAccountability);
|
|
30
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
31
|
+
expect(req.accountability).toEqual(customAccountability);
|
|
32
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
33
|
+
});
|
|
34
|
+
test('Uses default public accountability when no token is given', async () => {
|
|
35
|
+
const req = {
|
|
36
|
+
ip: '127.0.0.1',
|
|
37
|
+
get: jest.fn((string) => {
|
|
38
|
+
switch (string) {
|
|
39
|
+
case 'user-agent':
|
|
40
|
+
return 'fake-user-agent';
|
|
41
|
+
case 'origin':
|
|
42
|
+
return 'fake-origin';
|
|
43
|
+
default:
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
const res = {};
|
|
49
|
+
const next = jest.fn();
|
|
50
|
+
jest.spyOn(emitter_1.default, 'emitFilter').mockImplementation((_, payload) => payload);
|
|
51
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
52
|
+
expect(req.accountability).toEqual({
|
|
53
|
+
user: null,
|
|
54
|
+
role: null,
|
|
55
|
+
admin: false,
|
|
56
|
+
app: false,
|
|
57
|
+
ip: '127.0.0.1',
|
|
58
|
+
userAgent: 'fake-user-agent',
|
|
59
|
+
origin: 'fake-origin',
|
|
60
|
+
});
|
|
61
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
test('Sets accountability to payload contents if valid token is passed', async () => {
|
|
64
|
+
const userID = '3fac3c02-607f-4438-8d6e-6b8b25109b52';
|
|
65
|
+
const roleID = '38269fc6-6eb6-475a-93cb-479d97f73039';
|
|
66
|
+
const share = 'ca0ad005-f4ad-4bfe-b428-419ee8784790';
|
|
67
|
+
const shareScope = {
|
|
68
|
+
collection: 'articles',
|
|
69
|
+
item: 15,
|
|
70
|
+
};
|
|
71
|
+
const appAccess = true;
|
|
72
|
+
const adminAccess = false;
|
|
73
|
+
const token = jsonwebtoken_1.default.sign({
|
|
74
|
+
id: userID,
|
|
75
|
+
role: roleID,
|
|
76
|
+
app_access: appAccess,
|
|
77
|
+
admin_access: adminAccess,
|
|
78
|
+
share,
|
|
79
|
+
share_scope: shareScope,
|
|
80
|
+
}, env_1.default.SECRET, { issuer: 'directus' });
|
|
81
|
+
const req = {
|
|
82
|
+
ip: '127.0.0.1',
|
|
83
|
+
get: jest.fn((string) => {
|
|
84
|
+
switch (string) {
|
|
85
|
+
case 'user-agent':
|
|
86
|
+
return 'fake-user-agent';
|
|
87
|
+
case 'origin':
|
|
88
|
+
return 'fake-origin';
|
|
89
|
+
default:
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}),
|
|
93
|
+
token,
|
|
94
|
+
};
|
|
95
|
+
const res = {};
|
|
96
|
+
const next = jest.fn();
|
|
97
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
98
|
+
expect(req.accountability).toEqual({
|
|
99
|
+
user: userID,
|
|
100
|
+
role: roleID,
|
|
101
|
+
app: appAccess,
|
|
102
|
+
admin: adminAccess,
|
|
103
|
+
share,
|
|
104
|
+
share_scope: shareScope,
|
|
105
|
+
ip: '127.0.0.1',
|
|
106
|
+
userAgent: 'fake-user-agent',
|
|
107
|
+
origin: 'fake-origin',
|
|
108
|
+
});
|
|
109
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
110
|
+
// Test with 1/0 instead or true/false
|
|
111
|
+
next.mockClear();
|
|
112
|
+
req.token = jsonwebtoken_1.default.sign({
|
|
113
|
+
id: userID,
|
|
114
|
+
role: roleID,
|
|
115
|
+
app_access: 1,
|
|
116
|
+
admin_access: 0,
|
|
117
|
+
share,
|
|
118
|
+
share_scope: shareScope,
|
|
119
|
+
}, env_1.default.SECRET, { issuer: 'directus' });
|
|
120
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
121
|
+
expect(req.accountability).toEqual({
|
|
122
|
+
user: userID,
|
|
123
|
+
role: roleID,
|
|
124
|
+
app: appAccess,
|
|
125
|
+
admin: adminAccess,
|
|
126
|
+
share,
|
|
127
|
+
share_scope: shareScope,
|
|
128
|
+
ip: '127.0.0.1',
|
|
129
|
+
userAgent: 'fake-user-agent',
|
|
130
|
+
origin: 'fake-origin',
|
|
131
|
+
});
|
|
132
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
test('Throws InvalidCredentialsException when static token is used, but user does not exist', async () => {
|
|
135
|
+
jest.mocked(database_1.default).mockReturnValue({
|
|
136
|
+
select: jest.fn().mockReturnThis(),
|
|
137
|
+
from: jest.fn().mockReturnThis(),
|
|
138
|
+
leftJoin: jest.fn().mockReturnThis(),
|
|
139
|
+
where: jest.fn().mockReturnThis(),
|
|
140
|
+
first: jest.fn().mockResolvedValue(undefined),
|
|
141
|
+
});
|
|
142
|
+
const req = {
|
|
143
|
+
ip: '127.0.0.1',
|
|
144
|
+
get: jest.fn((string) => {
|
|
145
|
+
switch (string) {
|
|
146
|
+
case 'user-agent':
|
|
147
|
+
return 'fake-user-agent';
|
|
148
|
+
case 'origin':
|
|
149
|
+
return 'fake-origin';
|
|
150
|
+
default:
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}),
|
|
154
|
+
token: 'static-token',
|
|
155
|
+
};
|
|
156
|
+
const res = {};
|
|
157
|
+
const next = jest.fn();
|
|
158
|
+
expect((0, authenticate_1.handler)(req, res, next)).rejects.toEqual(new exceptions_1.InvalidCredentialsException());
|
|
159
|
+
expect(next).toHaveBeenCalledTimes(0);
|
|
160
|
+
});
|
|
161
|
+
test('Sets accountability to user information when static token is used', async () => {
|
|
162
|
+
const req = {
|
|
163
|
+
ip: '127.0.0.1',
|
|
164
|
+
get: jest.fn((string) => {
|
|
165
|
+
switch (string) {
|
|
166
|
+
case 'user-agent':
|
|
167
|
+
return 'fake-user-agent';
|
|
168
|
+
case 'origin':
|
|
169
|
+
return 'fake-origin';
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
token: 'static-token',
|
|
175
|
+
};
|
|
176
|
+
const res = {};
|
|
177
|
+
const next = jest.fn();
|
|
178
|
+
const testUser = { id: 'test-id', role: 'test-role', admin_access: true, app_access: false };
|
|
179
|
+
const expectedAccountability = {
|
|
180
|
+
user: testUser.id,
|
|
181
|
+
role: testUser.role,
|
|
182
|
+
app: testUser.app_access,
|
|
183
|
+
admin: testUser.admin_access,
|
|
184
|
+
ip: '127.0.0.1',
|
|
185
|
+
userAgent: 'fake-user-agent',
|
|
186
|
+
origin: 'fake-origin',
|
|
187
|
+
};
|
|
188
|
+
jest.mocked(database_1.default).mockReturnValue({
|
|
189
|
+
select: jest.fn().mockReturnThis(),
|
|
190
|
+
from: jest.fn().mockReturnThis(),
|
|
191
|
+
leftJoin: jest.fn().mockReturnThis(),
|
|
192
|
+
where: jest.fn().mockReturnThis(),
|
|
193
|
+
first: jest.fn().mockResolvedValue(testUser),
|
|
194
|
+
});
|
|
195
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
196
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
197
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
198
|
+
// Test for 0 / 1 instead of false / true
|
|
199
|
+
next.mockClear();
|
|
200
|
+
testUser.admin_access = 1;
|
|
201
|
+
testUser.app_access = 0;
|
|
202
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
203
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
204
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
205
|
+
// Test for "1" / "0" instead of true / false
|
|
206
|
+
next.mockClear();
|
|
207
|
+
testUser.admin_access = '0';
|
|
208
|
+
testUser.app_access = '1';
|
|
209
|
+
expectedAccountability.admin = false;
|
|
210
|
+
expectedAccountability.app = true;
|
|
211
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
212
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
213
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
214
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../../src/types/express.d.ts';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const extract_token_1 = __importDefault(require("../../src/middleware/extract-token"));
|
|
7
|
+
require("../../src/types/express.d.ts");
|
|
8
|
+
let mockRequest;
|
|
9
|
+
let mockResponse;
|
|
10
|
+
const nextFunction = jest.fn();
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockRequest = {};
|
|
13
|
+
mockResponse = {};
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
test('Token from query', () => {
|
|
17
|
+
mockRequest = {
|
|
18
|
+
query: {
|
|
19
|
+
access_token: 'test',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
(0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
|
|
23
|
+
expect(mockRequest.token).toBe('test');
|
|
24
|
+
expect(nextFunction).toBeCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
test('Token from Authorization header (capitalized)', () => {
|
|
27
|
+
mockRequest = {
|
|
28
|
+
headers: {
|
|
29
|
+
authorization: 'Bearer test',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
(0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
|
|
33
|
+
expect(mockRequest.token).toBe('test');
|
|
34
|
+
expect(nextFunction).toBeCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
test('Token from Authorization header (lowercase)', () => {
|
|
37
|
+
mockRequest = {
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: 'bearer test',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
(0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
|
|
43
|
+
expect(mockRequest.token).toBe('test');
|
|
44
|
+
expect(nextFunction).toBeCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
test('Ignore the token if authorization header is too many parts', () => {
|
|
47
|
+
mockRequest = {
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: 'bearer test what another one',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
(0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
|
|
53
|
+
expect(mockRequest.token).toBeNull();
|
|
54
|
+
expect(nextFunction).toBeCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
test('Null if no token passed', () => {
|
|
57
|
+
(0, extract_token_1.default)(mockRequest, mockResponse, nextFunction);
|
|
58
|
+
expect(mockRequest.token).toBeNull();
|
|
59
|
+
expect(nextFunction).toBeCalledTimes(1);
|
|
60
|
+
});
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => RequestHandler;
|
|
1
|
+
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
|
|
@@ -14,39 +14,36 @@ const validateBatch = (scope) => (0, async_handler_1.default)(async (req, res, n
|
|
|
14
14
|
req.body = {};
|
|
15
15
|
return next();
|
|
16
16
|
}
|
|
17
|
+
if (req.method.toLowerCase() !== 'search' && scope !== 'read' && req.singleton) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
17
20
|
if (!req.body)
|
|
18
21
|
throw new exceptions_1.InvalidPayloadException('Payload in body is required');
|
|
19
|
-
if (req.
|
|
22
|
+
if (['update', 'delete'].includes(scope) && Array.isArray(req.body)) {
|
|
20
23
|
return next();
|
|
24
|
+
}
|
|
25
|
+
// In reads, the query in the body should override the query params for searching
|
|
26
|
+
if (scope === 'read' && req.body.query) {
|
|
27
|
+
req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
|
|
28
|
+
}
|
|
21
29
|
// Every cRUD action has either keys or query
|
|
22
30
|
let batchSchema = joi_1.default.object().keys({
|
|
23
31
|
keys: joi_1.default.array().items(joi_1.default.alternatives(joi_1.default.string(), joi_1.default.number())),
|
|
24
32
|
query: joi_1.default.object().unknown(),
|
|
25
33
|
});
|
|
26
|
-
|
|
27
|
-
if (scope !== 'read') {
|
|
34
|
+
if (['update', 'delete'].includes(scope)) {
|
|
28
35
|
batchSchema = batchSchema.xor('query', 'keys');
|
|
29
36
|
}
|
|
30
37
|
// In updates, we add a required `data` that holds the update payload if an array isn't used
|
|
31
38
|
if (scope === 'update') {
|
|
32
|
-
if (Array.isArray(req.body))
|
|
33
|
-
return next();
|
|
34
39
|
batchSchema = batchSchema.keys({
|
|
35
40
|
data: joi_1.default.object().unknown().required(),
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
|
-
// In deletes, we want to keep supporting an array of just primary keys
|
|
39
|
-
if (scope === 'delete' && Array.isArray(req.body)) {
|
|
40
|
-
return next();
|
|
41
|
-
}
|
|
42
43
|
const { error } = batchSchema.validate(req.body);
|
|
43
44
|
if (error) {
|
|
44
45
|
throw new exceptions_2.FailedValidationException(error.details[0]);
|
|
45
46
|
}
|
|
46
|
-
// In reads, the query in the body should override the query params for searching
|
|
47
|
-
if (scope === 'read' && req.body.query) {
|
|
48
|
-
req.sanitizedQuery = (0, sanitize_query_1.sanitizeQuery)(req.body.query, req.accountability);
|
|
49
|
-
}
|
|
50
47
|
return next();
|
|
51
48
|
});
|
|
52
49
|
exports.validateBatch = validateBatch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '../../src/types/express.d.ts';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const validate_batch_1 = require("./validate-batch");
|
|
4
|
+
require("../../src/types/express.d.ts");
|
|
5
|
+
const exceptions_1 = require("../exceptions");
|
|
6
|
+
const exceptions_2 = require("@directus/shared/exceptions");
|
|
7
|
+
let mockRequest;
|
|
8
|
+
let mockResponse;
|
|
9
|
+
const nextFunction = jest.fn();
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockRequest = {};
|
|
12
|
+
mockResponse = {};
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
test('Sets body to empty, calls next on GET requests', async () => {
|
|
16
|
+
mockRequest.method = 'GET';
|
|
17
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
18
|
+
expect(mockRequest.body).toEqual({});
|
|
19
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
test(`Short circuits on singletons that aren't queried through SEARCH`, async () => {
|
|
22
|
+
mockRequest.method = 'PATCH';
|
|
23
|
+
mockRequest.singleton = true;
|
|
24
|
+
mockRequest.body = { title: 'test' };
|
|
25
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
26
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
test('Throws InvalidPayloadException on missing body', async () => {
|
|
29
|
+
mockRequest.method = 'SEARCH';
|
|
30
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
31
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_1.InvalidPayloadException);
|
|
33
|
+
});
|
|
34
|
+
test(`Short circuits on Array body in update/delete use`, async () => {
|
|
35
|
+
mockRequest.method = 'PATCH';
|
|
36
|
+
mockRequest.body = [1, 2, 3];
|
|
37
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
38
|
+
expect(mockRequest.sanitizedQuery).toBe(undefined);
|
|
39
|
+
expect(nextFunction).toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
|
|
42
|
+
mockRequest.method = 'SEARCH';
|
|
43
|
+
mockRequest.body = {
|
|
44
|
+
query: {
|
|
45
|
+
sort: 'id',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
await (0, validate_batch_1.validateBatch)('read')(mockRequest, mockResponse, nextFunction);
|
|
49
|
+
expect(mockRequest.sanitizedQuery).toEqual({
|
|
50
|
+
sort: ['id'],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
test(`Doesn't allow both query and keys in a batch delete`, async () => {
|
|
54
|
+
mockRequest.method = 'DELETE';
|
|
55
|
+
mockRequest.body = {
|
|
56
|
+
keys: [1, 2, 3],
|
|
57
|
+
query: { filter: {} },
|
|
58
|
+
};
|
|
59
|
+
await (0, validate_batch_1.validateBatch)('delete')(mockRequest, mockResponse, nextFunction);
|
|
60
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
|
|
62
|
+
});
|
|
63
|
+
test(`Requires 'data' on batch update`, async () => {
|
|
64
|
+
mockRequest.method = 'PATCH';
|
|
65
|
+
mockRequest.body = {
|
|
66
|
+
keys: [1, 2, 3],
|
|
67
|
+
query: { filter: {} },
|
|
68
|
+
};
|
|
69
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
70
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(exceptions_2.FailedValidationException);
|
|
72
|
+
});
|
|
73
|
+
test(`Calls next when all is well`, async () => {
|
|
74
|
+
mockRequest.method = 'PATCH';
|
|
75
|
+
mockRequest.body = {
|
|
76
|
+
query: { filter: {} },
|
|
77
|
+
data: {},
|
|
78
|
+
};
|
|
79
|
+
await (0, validate_batch_1.validateBatch)('update')(mockRequest, mockResponse, nextFunction);
|
|
80
|
+
expect(nextFunction).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeUndefined();
|
|
82
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@directus/shared/utils");
|
|
4
|
+
const vm2_1 = require("vm2");
|
|
5
|
+
exports.default = (0, utils_1.defineOperationApi)({
|
|
6
|
+
id: 'exec',
|
|
7
|
+
handler: async ({ code }, { data, env }) => {
|
|
8
|
+
const allowedModules = env.FLOWS_EXEC_ALLOWED_MODULES ? (0, utils_1.toArray)(env.FLOWS_EXEC_ALLOWED_MODULES) : [];
|
|
9
|
+
const opts = {
|
|
10
|
+
eval: false,
|
|
11
|
+
wasm: false,
|
|
12
|
+
};
|
|
13
|
+
if (allowedModules.length > 0) {
|
|
14
|
+
opts.require = {
|
|
15
|
+
external: {
|
|
16
|
+
modules: allowedModules,
|
|
17
|
+
transitive: false,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const vm = new vm2_1.NodeVM(opts);
|
|
22
|
+
const script = new vm2_1.VMScript(code).compile();
|
|
23
|
+
const fn = await vm.run(script);
|
|
24
|
+
return await fn(data);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|