directus 9.14.5 → 9.16.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 +2 -1
- package/dist/auth/drivers/ldap.js +18 -8
- package/dist/auth/drivers/oauth2.js +19 -9
- package/dist/auth/drivers/openid.js +19 -9
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +21 -2
- 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 +2 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -1
- package/dist/controllers/assets.js +27 -1
- package/dist/controllers/extensions.js +3 -2
- 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/helpers/schema/dialects/cockroachdb.d.ts +3 -14
- package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -8
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -10
- package/dist/database/helpers/schema/dialects/oracle.js +2 -5
- package/dist/database/helpers/schema/types.d.ts +8 -18
- package/dist/database/helpers/schema/types.js +7 -36
- package/dist/database/migrations/20201105B-change-webhook-url-type.js +3 -2
- package/dist/database/migrations/20210312A-webhooks-collections-text.js +3 -2
- package/dist/database/migrations/20210415A-make-filesize-nullable.js +2 -2
- package/dist/database/migrations/20210510A-restructure-relations.js +3 -3
- package/dist/database/migrations/20210903A-add-auth-provider.js +2 -2
- package/dist/database/migrations/20210907A-webhooks-collections-not-null.js +4 -2
- package/dist/database/migrations/20210920A-webhooks-url-not-null.js +2 -2
- package/dist/database/migrations/20220303A-remove-default-project-color.js +2 -2
- package/dist/database/migrations/20220325B-add-default-language.js +2 -2
- package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +2 -2
- package/dist/database/migrations/20220801A-update-notifications-timestamp-column.d.ts +3 -0
- package/dist/database/migrations/20220801A-update-notifications-timestamp-column.js +19 -0
- package/dist/database/migrations/20220802A-add-custom-aspect-ratios.d.ts +3 -0
- package/dist/database/migrations/20220802A-add-custom-aspect-ratios.js +15 -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/settings.yaml +33 -0
- package/dist/env.js +8 -0
- package/dist/env.test.d.ts +8 -0
- package/dist/env.test.js +39 -0
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +10 -9
- package/dist/flows.js +2 -1
- package/dist/logger.js +0 -1
- package/dist/middleware/authenticate.test.d.ts +1 -0
- package/dist/middleware/authenticate.test.js +174 -0
- package/dist/middleware/cache.js +3 -3
- package/dist/middleware/extract-token.test.d.ts +1 -0
- package/dist/middleware/extract-token.test.js +60 -0
- package/dist/middleware/respond.js +2 -2
- 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/item-create/index.js +1 -1
- package/dist/operations/item-delete/index.js +2 -2
- package/dist/operations/item-read/index.js +2 -2
- package/dist/operations/item-update/index.js +3 -3
- package/dist/operations/notification/index.js +9 -6
- package/dist/operations/request/index.js +22 -3
- package/dist/services/assets.d.ts +1 -1
- package/dist/services/assets.js +7 -2
- package/dist/services/authorization.js +2 -1
- 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 +59 -40
- package/dist/services/graphql/types/hash.d.ts +2 -0
- package/dist/services/graphql/types/hash.js +9 -0
- package/dist/services/items.js +83 -39
- 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 +35 -8
- package/dist/services/payload.test.d.ts +1 -0
- package/dist/services/payload.test.js +94 -0
- package/dist/services/server.js +5 -3
- package/dist/services/specifications.js +1 -6
- 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 +15 -0
- 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/calculate-field-depth.test.d.ts +1 -0
- package/dist/utils/calculate-field-depth.test.js +76 -0
- package/dist/utils/compress.d.ts +3 -0
- package/dist/utils/compress.js +17 -0
- package/dist/utils/filter-items.test.d.ts +1 -0
- package/dist/utils/filter-items.test.js +60 -0
- package/dist/utils/get-ast-from-query.js +1 -1
- 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 +7 -1
- package/dist/utils/get-os-info.d.ts +9 -0
- package/dist/utils/get-os-info.js +47 -0
- package/dist/utils/get-permissions.js +2 -2
- 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-schema.js +1 -2
- 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 +12 -11
|
@@ -0,0 +1,174 @@
|
|
|
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) => (string === 'user-agent' ? 'fake-user-agent' : null)),
|
|
38
|
+
};
|
|
39
|
+
const res = {};
|
|
40
|
+
const next = jest.fn();
|
|
41
|
+
jest.spyOn(emitter_1.default, 'emitFilter').mockImplementation((_, payload) => payload);
|
|
42
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
43
|
+
expect(req.accountability).toEqual({
|
|
44
|
+
user: null,
|
|
45
|
+
role: null,
|
|
46
|
+
admin: false,
|
|
47
|
+
app: false,
|
|
48
|
+
ip: '127.0.0.1',
|
|
49
|
+
userAgent: 'fake-user-agent',
|
|
50
|
+
});
|
|
51
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
test('Sets accountability to payload contents if valid token is passed', async () => {
|
|
54
|
+
const userID = '3fac3c02-607f-4438-8d6e-6b8b25109b52';
|
|
55
|
+
const roleID = '38269fc6-6eb6-475a-93cb-479d97f73039';
|
|
56
|
+
const share = 'ca0ad005-f4ad-4bfe-b428-419ee8784790';
|
|
57
|
+
const shareScope = {
|
|
58
|
+
collection: 'articles',
|
|
59
|
+
item: 15,
|
|
60
|
+
};
|
|
61
|
+
const appAccess = true;
|
|
62
|
+
const adminAccess = false;
|
|
63
|
+
const token = jsonwebtoken_1.default.sign({
|
|
64
|
+
id: userID,
|
|
65
|
+
role: roleID,
|
|
66
|
+
app_access: appAccess,
|
|
67
|
+
admin_access: adminAccess,
|
|
68
|
+
share,
|
|
69
|
+
share_scope: shareScope,
|
|
70
|
+
}, env_1.default.SECRET, { issuer: 'directus' });
|
|
71
|
+
const req = {
|
|
72
|
+
ip: '127.0.0.1',
|
|
73
|
+
get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
|
|
74
|
+
token,
|
|
75
|
+
};
|
|
76
|
+
const res = {};
|
|
77
|
+
const next = jest.fn();
|
|
78
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
79
|
+
expect(req.accountability).toEqual({
|
|
80
|
+
user: userID,
|
|
81
|
+
role: roleID,
|
|
82
|
+
app: appAccess,
|
|
83
|
+
admin: adminAccess,
|
|
84
|
+
share,
|
|
85
|
+
share_scope: shareScope,
|
|
86
|
+
ip: '127.0.0.1',
|
|
87
|
+
userAgent: 'fake-user-agent',
|
|
88
|
+
});
|
|
89
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
90
|
+
// Test with 1/0 instead or true/false
|
|
91
|
+
next.mockClear();
|
|
92
|
+
req.token = jsonwebtoken_1.default.sign({
|
|
93
|
+
id: userID,
|
|
94
|
+
role: roleID,
|
|
95
|
+
app_access: 1,
|
|
96
|
+
admin_access: 0,
|
|
97
|
+
share,
|
|
98
|
+
share_scope: shareScope,
|
|
99
|
+
}, env_1.default.SECRET, { issuer: 'directus' });
|
|
100
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
101
|
+
expect(req.accountability).toEqual({
|
|
102
|
+
user: userID,
|
|
103
|
+
role: roleID,
|
|
104
|
+
app: appAccess,
|
|
105
|
+
admin: adminAccess,
|
|
106
|
+
share,
|
|
107
|
+
share_scope: shareScope,
|
|
108
|
+
ip: '127.0.0.1',
|
|
109
|
+
userAgent: 'fake-user-agent',
|
|
110
|
+
});
|
|
111
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
test('Throws InvalidCredentialsException when static token is used, but user does not exist', async () => {
|
|
114
|
+
jest.mocked(database_1.default).mockReturnValue({
|
|
115
|
+
select: jest.fn().mockReturnThis(),
|
|
116
|
+
from: jest.fn().mockReturnThis(),
|
|
117
|
+
leftJoin: jest.fn().mockReturnThis(),
|
|
118
|
+
where: jest.fn().mockReturnThis(),
|
|
119
|
+
first: jest.fn().mockResolvedValue(undefined),
|
|
120
|
+
});
|
|
121
|
+
const req = {
|
|
122
|
+
ip: '127.0.0.1',
|
|
123
|
+
get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
|
|
124
|
+
token: 'static-token',
|
|
125
|
+
};
|
|
126
|
+
const res = {};
|
|
127
|
+
const next = jest.fn();
|
|
128
|
+
expect((0, authenticate_1.handler)(req, res, next)).rejects.toEqual(new exceptions_1.InvalidCredentialsException());
|
|
129
|
+
expect(next).toHaveBeenCalledTimes(0);
|
|
130
|
+
});
|
|
131
|
+
test('Sets accountability to user information when static token is used', async () => {
|
|
132
|
+
const req = {
|
|
133
|
+
ip: '127.0.0.1',
|
|
134
|
+
get: jest.fn((string) => (string === 'user-agent' ? 'fake-user-agent' : null)),
|
|
135
|
+
token: 'static-token',
|
|
136
|
+
};
|
|
137
|
+
const res = {};
|
|
138
|
+
const next = jest.fn();
|
|
139
|
+
const testUser = { id: 'test-id', role: 'test-role', admin_access: true, app_access: false };
|
|
140
|
+
const expectedAccountability = {
|
|
141
|
+
user: testUser.id,
|
|
142
|
+
role: testUser.role,
|
|
143
|
+
app: testUser.app_access,
|
|
144
|
+
admin: testUser.admin_access,
|
|
145
|
+
ip: '127.0.0.1',
|
|
146
|
+
userAgent: 'fake-user-agent',
|
|
147
|
+
};
|
|
148
|
+
jest.mocked(database_1.default).mockReturnValue({
|
|
149
|
+
select: jest.fn().mockReturnThis(),
|
|
150
|
+
from: jest.fn().mockReturnThis(),
|
|
151
|
+
leftJoin: jest.fn().mockReturnThis(),
|
|
152
|
+
where: jest.fn().mockReturnThis(),
|
|
153
|
+
first: jest.fn().mockResolvedValue(testUser),
|
|
154
|
+
});
|
|
155
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
156
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
157
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
158
|
+
// Test for 0 / 1 instead of false / true
|
|
159
|
+
next.mockClear();
|
|
160
|
+
testUser.admin_access = 1;
|
|
161
|
+
testUser.app_access = 0;
|
|
162
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
163
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
164
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
165
|
+
// Test for "1" / "0" instead of true / false
|
|
166
|
+
next.mockClear();
|
|
167
|
+
testUser.admin_access = '0';
|
|
168
|
+
testUser.app_access = '1';
|
|
169
|
+
expectedAccountability.admin = false;
|
|
170
|
+
expectedAccountability.app = true;
|
|
171
|
+
await (0, authenticate_1.handler)(req, res, next);
|
|
172
|
+
expect(req.accountability).toEqual(expectedAccountability);
|
|
173
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
package/dist/middleware/cache.js
CHANGED
|
@@ -10,7 +10,7 @@ const get_cache_headers_1 = require("../utils/get-cache-headers");
|
|
|
10
10
|
const get_cache_key_1 = require("../utils/get-cache-key");
|
|
11
11
|
const logger_1 = __importDefault(require("../logger"));
|
|
12
12
|
const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next) => {
|
|
13
|
-
var _a, _b, _c;
|
|
13
|
+
var _a, _b, _c, _d;
|
|
14
14
|
const { cache } = (0, cache_1.getCache)();
|
|
15
15
|
if (req.method.toLowerCase() !== 'get' && ((_a = req.path) === null || _a === void 0 ? void 0 : _a.startsWith('/graphql')) === false)
|
|
16
16
|
return next();
|
|
@@ -26,7 +26,7 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
|
|
|
26
26
|
const key = (0, get_cache_key_1.getCacheKey)(req);
|
|
27
27
|
let cachedData;
|
|
28
28
|
try {
|
|
29
|
-
cachedData = await
|
|
29
|
+
cachedData = await (0, cache_1.getCacheValue)(cache, key);
|
|
30
30
|
}
|
|
31
31
|
catch (err) {
|
|
32
32
|
logger_1.default.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`);
|
|
@@ -37,7 +37,7 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
|
|
|
37
37
|
if (cachedData) {
|
|
38
38
|
let cacheExpiryDate;
|
|
39
39
|
try {
|
|
40
|
-
cacheExpiryDate = (await
|
|
40
|
+
cacheExpiryDate = (_d = (await (0, cache_1.getCacheValue)(cache, `${key}__expires_at`))) === null || _d === void 0 ? void 0 : _d.exp;
|
|
41
41
|
}
|
|
42
42
|
catch (err) {
|
|
43
43
|
logger_1.default.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`);
|
|
@@ -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
|
+
});
|
|
@@ -32,8 +32,8 @@ exports.respond = (0, async_handler_1.default)(async (req, res) => {
|
|
|
32
32
|
exceedsMaxSize === false) {
|
|
33
33
|
const key = (0, get_cache_key_1.getCacheKey)(req);
|
|
34
34
|
try {
|
|
35
|
-
await
|
|
36
|
-
await
|
|
35
|
+
await (0, cache_1.setCacheValue)(cache, key, res.locals.payload, (0, ms_1.default)(env_1.default.CACHE_TTL));
|
|
36
|
+
await (0, cache_1.setCacheValue)(cache, `${key}__expires_at`, { exp: Date.now() + (0, ms_1.default)(env_1.default.CACHE_TTL) });
|
|
37
37
|
}
|
|
38
38
|
catch (err) {
|
|
39
39
|
logger_1.default.warn(err, `[cache] Couldn't set key ${key}. ${err}`);
|
|
@@ -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 {};
|
|
@@ -0,0 +1,95 @@
|
|
|
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 vm2_1 = require("vm2");
|
|
7
|
+
const index_1 = __importDefault(require("./index"));
|
|
8
|
+
test('Rejects when modules are used without modules being allowed', async () => {
|
|
9
|
+
const testCode = `
|
|
10
|
+
const test = require('test');
|
|
11
|
+
`;
|
|
12
|
+
await expect(index_1.default.handler({ code: testCode }, {
|
|
13
|
+
data: {},
|
|
14
|
+
env: {
|
|
15
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
16
|
+
},
|
|
17
|
+
})).rejects.toEqual(new vm2_1.VMError("Cannot find module 'test'"));
|
|
18
|
+
});
|
|
19
|
+
test('Rejects when code contains syntax errors', async () => {
|
|
20
|
+
const testCode = `
|
|
21
|
+
~~
|
|
22
|
+
`;
|
|
23
|
+
await expect(index_1.default.handler({ code: testCode }, {
|
|
24
|
+
data: {},
|
|
25
|
+
env: {
|
|
26
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
27
|
+
},
|
|
28
|
+
})).rejects.toEqual(new Error("Couldn't compile code: Unexpected end of input"));
|
|
29
|
+
});
|
|
30
|
+
test('Rejects when returned function does something illegal', async () => {
|
|
31
|
+
const testCode = `
|
|
32
|
+
module.exports = function() {
|
|
33
|
+
return a + b;
|
|
34
|
+
};
|
|
35
|
+
`;
|
|
36
|
+
await expect(index_1.default.handler({ code: testCode }, {
|
|
37
|
+
data: {},
|
|
38
|
+
env: {
|
|
39
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
40
|
+
},
|
|
41
|
+
})).rejects.toEqual(new ReferenceError('a is not defined'));
|
|
42
|
+
});
|
|
43
|
+
test("Rejects when code doesn't return valid function", async () => {
|
|
44
|
+
const testCode = `
|
|
45
|
+
module.exports = false;
|
|
46
|
+
`;
|
|
47
|
+
await expect(index_1.default.handler({ code: testCode }, {
|
|
48
|
+
data: {},
|
|
49
|
+
env: {
|
|
50
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
51
|
+
},
|
|
52
|
+
})).rejects.toEqual(new TypeError('fn is not a function'));
|
|
53
|
+
});
|
|
54
|
+
test('Rejects returned function throws errors', async () => {
|
|
55
|
+
const testCode = `
|
|
56
|
+
module.exports = function () {
|
|
57
|
+
throw new Error('test');
|
|
58
|
+
};
|
|
59
|
+
`;
|
|
60
|
+
await expect(index_1.default.handler({ code: testCode }, {
|
|
61
|
+
data: {},
|
|
62
|
+
env: {
|
|
63
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
64
|
+
},
|
|
65
|
+
})).rejects.toEqual(new Error('test'));
|
|
66
|
+
});
|
|
67
|
+
test('Executes function when valid', () => {
|
|
68
|
+
const testCode = `
|
|
69
|
+
module.exports = function (data) {
|
|
70
|
+
return { result: data.input + ' test' };
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
expect(index_1.default.handler({ code: testCode }, {
|
|
74
|
+
data: {
|
|
75
|
+
input: 'start',
|
|
76
|
+
},
|
|
77
|
+
env: {
|
|
78
|
+
FLOWS_EXEC_ALLOWED_MODULES: '',
|
|
79
|
+
},
|
|
80
|
+
})).resolves.toEqual({ result: 'start test' });
|
|
81
|
+
});
|
|
82
|
+
test('Allows modules that are whitelisted', () => {
|
|
83
|
+
const testCode = `
|
|
84
|
+
const bytes = require('bytes');
|
|
85
|
+
|
|
86
|
+
module.exports = function (data) {
|
|
87
|
+
return { result: bytes(1000) };
|
|
88
|
+
};
|
|
89
|
+
`;
|
|
90
|
+
expect(index_1.default.handler({ code: testCode }, {
|
|
91
|
+
env: {
|
|
92
|
+
FLOWS_EXEC_ALLOWED_MODULES: 'bytes',
|
|
93
|
+
},
|
|
94
|
+
})).resolves.toEqual({ result: '1000B' });
|
|
95
|
+
});
|
|
@@ -32,7 +32,7 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
32
32
|
result = null;
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
35
|
-
result = await itemsService.createMany((0, utils_1.toArray)(payloadObject), { emitEvents });
|
|
35
|
+
result = await itemsService.createMany((0, utils_1.toArray)(payloadObject), { emitEvents: !!emitEvents });
|
|
36
36
|
}
|
|
37
37
|
return result;
|
|
38
38
|
},
|
|
@@ -35,10 +35,10 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
35
35
|
else {
|
|
36
36
|
const keys = (0, utils_1.toArray)(key);
|
|
37
37
|
if (keys.length === 1) {
|
|
38
|
-
result = await itemsService.deleteOne(keys[0], { emitEvents });
|
|
38
|
+
result = await itemsService.deleteOne(keys[0], { emitEvents: !!emitEvents });
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
|
-
result = await itemsService.deleteMany(keys, { emitEvents });
|
|
41
|
+
result = await itemsService.deleteMany(keys, { emitEvents: !!emitEvents });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
return result;
|
|
@@ -35,10 +35,10 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
35
35
|
else {
|
|
36
36
|
const keys = (0, utils_1.toArray)(key);
|
|
37
37
|
if (keys.length === 1) {
|
|
38
|
-
result = await itemsService.readOne(keys[0], sanitizedQueryObject, { emitEvents });
|
|
38
|
+
result = await itemsService.readOne(keys[0], sanitizedQueryObject, { emitEvents: !!emitEvents });
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
|
-
result = await itemsService.readMany(keys, sanitizedQueryObject, { emitEvents });
|
|
41
|
+
result = await itemsService.readMany(keys, sanitizedQueryObject, { emitEvents: !!emitEvents });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
return result;
|
|
@@ -35,15 +35,15 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
35
35
|
}
|
|
36
36
|
let result;
|
|
37
37
|
if (!key || (Array.isArray(key) && key.length === 0)) {
|
|
38
|
-
result = await itemsService.updateByQuery(sanitizedQueryObject, payloadObject, { emitEvents });
|
|
38
|
+
result = await itemsService.updateByQuery(sanitizedQueryObject, payloadObject, { emitEvents: !!emitEvents });
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
41
|
const keys = (0, utils_1.toArray)(key);
|
|
42
42
|
if (keys.length === 1) {
|
|
43
|
-
result = await itemsService.updateOne(keys[0], payloadObject, { emitEvents });
|
|
43
|
+
result = await itemsService.updateOne(keys[0], payloadObject, { emitEvents: !!emitEvents });
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
result = await itemsService.updateMany(keys, payloadObject, { emitEvents });
|
|
46
|
+
result = await itemsService.updateMany(keys, payloadObject, { emitEvents: !!emitEvents });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
return result;
|
|
@@ -6,7 +6,6 @@ const get_accountability_for_role_1 = require("../../utils/get-accountability-fo
|
|
|
6
6
|
exports.default = (0, utils_1.defineOperationApi)({
|
|
7
7
|
id: 'notification',
|
|
8
8
|
handler: async ({ recipient, subject, message, permissions }, { accountability, database, getSchema }) => {
|
|
9
|
-
var _a;
|
|
10
9
|
const schema = await getSchema({ database });
|
|
11
10
|
let customAccountability;
|
|
12
11
|
if (!permissions || permissions === '$trigger') {
|
|
@@ -27,12 +26,16 @@ exports.default = (0, utils_1.defineOperationApi)({
|
|
|
27
26
|
knex: database,
|
|
28
27
|
});
|
|
29
28
|
const messageString = message ? (0, utils_1.optionToString)(message) : null;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const payload = (0, utils_1.toArray)(recipient).map((userId) => {
|
|
30
|
+
var _a;
|
|
31
|
+
return {
|
|
32
|
+
recipient: userId,
|
|
33
|
+
sender: (_a = customAccountability === null || customAccountability === void 0 ? void 0 : customAccountability.user) !== null && _a !== void 0 ? _a : null,
|
|
34
|
+
subject,
|
|
35
|
+
message: messageString,
|
|
36
|
+
};
|
|
35
37
|
});
|
|
38
|
+
const result = await notificationsService.createMany(payload);
|
|
36
39
|
return result;
|
|
37
40
|
},
|
|
38
41
|
});
|
|
@@ -8,11 +8,30 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
8
8
|
exports.default = (0, utils_1.defineOperationApi)({
|
|
9
9
|
id: 'request',
|
|
10
10
|
handler: async ({ url, method, body, headers }) => {
|
|
11
|
-
|
|
11
|
+
var _a;
|
|
12
|
+
const customHeaders = (_a = headers === null || headers === void 0 ? void 0 : headers.reduce((acc, { header, value }) => {
|
|
12
13
|
acc[header] = value;
|
|
13
14
|
return acc;
|
|
14
|
-
}, {});
|
|
15
|
-
|
|
15
|
+
}, {})) !== null && _a !== void 0 ? _a : {};
|
|
16
|
+
if (!customHeaders['Content-Type'] && isValidJSON(body)) {
|
|
17
|
+
customHeaders['Content-Type'] = 'application/json';
|
|
18
|
+
}
|
|
19
|
+
const shouldEncode = decodeURI(url) === url;
|
|
20
|
+
const result = await (0, axios_1.default)({
|
|
21
|
+
url: shouldEncode ? encodeURI(url) : url,
|
|
22
|
+
method,
|
|
23
|
+
data: body,
|
|
24
|
+
headers: customHeaders,
|
|
25
|
+
});
|
|
16
26
|
return { status: result.status, statusText: result.statusText, headers: result.headers, data: result.data };
|
|
27
|
+
function isValidJSON(value) {
|
|
28
|
+
try {
|
|
29
|
+
(0, utils_1.parseJSON)(value);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
17
36
|
},
|
|
18
37
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { Range, StatResponse } from '@directus/drive';
|
|
3
|
+
import { Accountability } from '@directus/shared/types';
|
|
3
4
|
import { Knex } from 'knex';
|
|
4
5
|
import { AbstractServiceOptions, TransformationParams, TransformationPreset } from '../types';
|
|
5
|
-
import { Accountability } from '@directus/shared/types';
|
|
6
6
|
import { AuthorizationService } from './authorization';
|
|
7
7
|
export declare class AssetsService {
|
|
8
8
|
knex: Knex;
|
package/dist/services/assets.js
CHANGED
|
@@ -32,13 +32,14 @@ const mime_types_1 = require("mime-types");
|
|
|
32
32
|
const object_hash_1 = __importDefault(require("object-hash"));
|
|
33
33
|
const path_1 = __importDefault(require("path"));
|
|
34
34
|
const sharp_1 = __importDefault(require("sharp"));
|
|
35
|
+
const uuid_validate_1 = __importDefault(require("uuid-validate"));
|
|
35
36
|
const database_1 = __importDefault(require("../database"));
|
|
36
37
|
const env_1 = __importDefault(require("../env"));
|
|
37
38
|
const exceptions_1 = require("../exceptions");
|
|
39
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
38
40
|
const storage_1 = __importDefault(require("../storage"));
|
|
39
|
-
const authorization_1 = require("./authorization");
|
|
40
41
|
const TransformationUtils = __importStar(require("../utils/transformations"));
|
|
41
|
-
const
|
|
42
|
+
const authorization_1 = require("./authorization");
|
|
42
43
|
sharp_1.default.concurrency(1);
|
|
43
44
|
// Note: don't put this in the service. The service can be initialized in multiple places, but they
|
|
44
45
|
// should all share the same semaphore instance.
|
|
@@ -140,6 +141,10 @@ class AssetsService {
|
|
|
140
141
|
sequentialRead: true,
|
|
141
142
|
}).rotate();
|
|
142
143
|
transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
|
|
144
|
+
readStream.on('error', (e) => {
|
|
145
|
+
logger_1.default.error(e, `Couldn't transform file ${file.id}`);
|
|
146
|
+
readStream.unpipe(transformer);
|
|
147
|
+
});
|
|
143
148
|
await storage_1.default.disk(file.storage).put(assetFilename, readStream.pipe(transformer), type);
|
|
144
149
|
return {
|
|
145
150
|
stream: storage_1.default.disk(file.storage).getStream(assetFilename, range),
|
|
@@ -13,6 +13,7 @@ const strip_function_1 = require("../utils/strip-function");
|
|
|
13
13
|
const items_1 = require("./items");
|
|
14
14
|
const payload_1 = require("./payload");
|
|
15
15
|
const get_relation_info_1 = require("../utils/get-relation-info");
|
|
16
|
+
const constants_1 = require("../constants");
|
|
16
17
|
class AuthorizationService {
|
|
17
18
|
constructor(options) {
|
|
18
19
|
this.knex = options.knex || (0, database_1.default)();
|
|
@@ -379,7 +380,7 @@ class AuthorizationService {
|
|
|
379
380
|
const requiredColumns = [];
|
|
380
381
|
for (const field of Object.values(this.schema.collections[collection].fields)) {
|
|
381
382
|
const specials = (_g = field === null || field === void 0 ? void 0 : field.special) !== null && _g !== void 0 ? _g : [];
|
|
382
|
-
const hasGenerateSpecial =
|
|
383
|
+
const hasGenerateSpecial = constants_1.GENERATE_SPECIAL.some((name) => specials.includes(name));
|
|
383
384
|
const nullable = field.nullable || hasGenerateSpecial || field.generated;
|
|
384
385
|
if (!nullable) {
|
|
385
386
|
requiredColumns.push(field);
|
package/dist/services/files.js
CHANGED
|
@@ -229,7 +229,8 @@ class FilesService extends items_1.ItemsService {
|
|
|
229
229
|
}
|
|
230
230
|
let fileResponse;
|
|
231
231
|
try {
|
|
232
|
-
|
|
232
|
+
const shouldEncode = decodeURI(importURL) === importURL;
|
|
233
|
+
fileResponse = await axios_1.default.get(shouldEncode ? encodeURI(importURL) : importURL, {
|
|
233
234
|
responseType: 'stream',
|
|
234
235
|
});
|
|
235
236
|
}
|
|
@@ -240,7 +241,7 @@ class FilesService extends items_1.ItemsService {
|
|
|
240
241
|
});
|
|
241
242
|
}
|
|
242
243
|
const parsedURL = url_1.default.parse(fileResponse.request.res.responseUrl);
|
|
243
|
-
const filename = path_1.default.basename(parsedURL.pathname);
|
|
244
|
+
const filename = decodeURI(path_1.default.basename(parsedURL.pathname));
|
|
244
245
|
const payload = {
|
|
245
246
|
filename_download: filename,
|
|
246
247
|
storage: (0, utils_1.toArray)(env_1.default.STORAGE_LOCATIONS)[0],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
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 exifr_1 = __importDefault(require("exifr"));
|
|
7
|
+
const knex_1 = __importDefault(require("knex"));
|
|
8
|
+
const knex_mock_client_1 = require("knex-mock-client");
|
|
9
|
+
const _1 = require(".");
|
|
10
|
+
jest.mock('exifr');
|
|
11
|
+
jest.mock('../../src/database/index', () => {
|
|
12
|
+
return { getDatabaseClient: jest.fn().mockReturnValue('postgres') };
|
|
13
|
+
});
|
|
14
|
+
jest.requireMock('../../src/database/index');
|
|
15
|
+
describe('Integration Tests', () => {
|
|
16
|
+
let db;
|
|
17
|
+
let tracker;
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
db = (0, knex_1.default)({ client: knex_mock_client_1.MockClient });
|
|
20
|
+
tracker = (0, knex_mock_client_1.getTracker)();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
tracker.reset();
|
|
24
|
+
});
|
|
25
|
+
describe('Services / Files', () => {
|
|
26
|
+
describe('getMetadata', () => {
|
|
27
|
+
let service;
|
|
28
|
+
let exifrParseSpy;
|
|
29
|
+
const sampleMetadata = {
|
|
30
|
+
CustomTagA: 'value a',
|
|
31
|
+
CustomTagB: 'value b',
|
|
32
|
+
CustomTagC: 'value c',
|
|
33
|
+
};
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
exifrParseSpy = jest.spyOn(exifr_1.default, 'parse');
|
|
36
|
+
service = new _1.FilesService({
|
|
37
|
+
knex: db,
|
|
38
|
+
schema: { collections: {}, relations: [] },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('accepts allowlist metadata tags', async () => {
|
|
42
|
+
exifrParseSpy.mockReturnValue(Promise.resolve({ ...sampleMetadata }));
|
|
43
|
+
const bufferContent = 'file buffer content';
|
|
44
|
+
const allowList = ['CustomTagB', 'CustomTagA'];
|
|
45
|
+
const metadata = await service.getMetadata(bufferContent, allowList);
|
|
46
|
+
expect(exifrParseSpy).toHaveBeenCalled();
|
|
47
|
+
expect(metadata.metadata.CustomTagA).toStrictEqual(sampleMetadata.CustomTagA);
|
|
48
|
+
expect(metadata.metadata.CustomTagB).toStrictEqual(sampleMetadata.CustomTagB);
|
|
49
|
+
expect(metadata.metadata.CustomTagC).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|