directus 9.7.1 → 9.9.1
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/__mocks__/cache.d.ts +5 -0
- package/dist/__mocks__/cache.js +7 -0
- package/dist/auth/drivers/ldap.js +10 -11
- package/dist/auth/drivers/oauth2.js +9 -4
- package/dist/auth/drivers/openid.js +7 -4
- package/dist/cache.js +2 -2
- package/dist/cli/commands/schema/apply.js +9 -3
- package/dist/controllers/assets.js +5 -0
- package/dist/controllers/files.d.ts +2 -0
- package/dist/controllers/files.js +13 -5
- package/dist/database/helpers/date/dialects/default.d.ts +3 -0
- package/dist/database/helpers/date/dialects/default.js +7 -0
- package/dist/database/helpers/date/dialects/mssql.d.ts +1 -9
- package/dist/database/helpers/date/dialects/mssql.js +4 -23
- package/dist/database/helpers/date/dialects/mysql.d.ts +2 -9
- package/dist/database/helpers/date/dialects/mysql.js +7 -22
- package/dist/database/helpers/date/dialects/oracle.d.ts +1 -9
- package/dist/database/helpers/date/dialects/oracle.js +7 -23
- package/dist/database/helpers/date/dialects/sqlite.d.ts +1 -9
- package/dist/database/helpers/date/dialects/sqlite.js +8 -24
- package/dist/database/helpers/date/index.d.ts +4 -4
- package/dist/database/helpers/date/index.js +9 -9
- package/dist/database/helpers/date/types.d.ts +3 -9
- package/dist/database/helpers/date/types.js +10 -0
- package/dist/database/helpers/fn/dialects/mssql.d.ts +13 -0
- package/dist/database/helpers/fn/dialects/mssql.js +42 -0
- package/dist/database/helpers/{date/dialects/postgres.d.ts → fn/dialects/mysql.d.ts} +3 -2
- package/dist/database/helpers/fn/dialects/mysql.js +42 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +13 -0
- package/dist/database/helpers/fn/dialects/oracle.js +42 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +13 -0
- package/dist/database/helpers/{date → fn}/dialects/postgres.js +18 -3
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +13 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +42 -0
- package/dist/database/helpers/fn/index.d.ts +7 -0
- package/dist/database/helpers/fn/index.js +17 -0
- package/dist/database/helpers/fn/types.d.ts +18 -0
- package/dist/database/helpers/fn/types.js +27 -0
- package/dist/database/helpers/index.d.ts +4 -1
- package/dist/database/helpers/index.js +7 -1
- package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.d.ts +3 -0
- package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.js +17 -0
- package/dist/database/migrations/20220322A-rename-field-typecast-flags.js +6 -2
- package/dist/database/migrations/20220323A-add-field-validation.d.ts +3 -0
- package/dist/database/migrations/20220323A-add-field-validation.js +17 -0
- package/dist/database/migrations/20220325A-fix-typecast-flags.d.ts +3 -0
- package/dist/database/migrations/20220325A-fix-typecast-flags.js +49 -0
- package/dist/database/migrations/20220325B-add-default-language.d.ts +3 -0
- package/dist/database/migrations/20220325B-add-default-language.js +28 -0
- package/dist/database/migrations/20220402A-remove-default-value-panel-icon.d.ts +3 -0
- package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +22 -0
- package/dist/database/run-ast.js +7 -5
- package/dist/database/system-data/fields/activity.yaml +4 -4
- package/dist/database/system-data/fields/collections.yaml +1 -1
- package/dist/database/system-data/fields/fields.yaml +9 -0
- package/dist/database/system-data/fields/presets.yaml +14 -0
- package/dist/database/system-data/fields/settings.yaml +12 -1
- package/dist/database/system-data/fields/users.yaml +3 -0
- package/dist/env.js +5 -3
- package/dist/exceptions/index.d.ts +1 -0
- package/dist/exceptions/index.js +1 -0
- package/dist/exceptions/token-expired.d.ts +4 -0
- package/dist/exceptions/token-expired.js +10 -0
- package/dist/logger.js +2 -1
- package/dist/middleware/cache.js +10 -0
- package/dist/services/activity.js +4 -1
- package/dist/services/authorization.d.ts +1 -1
- package/dist/services/authorization.js +174 -48
- package/dist/services/collections.d.ts +2 -0
- package/dist/services/collections.js +232 -198
- package/dist/services/fields.js +210 -174
- package/dist/services/files.d.ts +5 -1
- package/dist/services/files.js +59 -40
- package/dist/services/graphql.d.ts +2 -3
- package/dist/services/graphql.js +53 -10
- package/dist/services/items.js +5 -3
- package/dist/services/payload.d.ts +2 -1
- package/dist/services/payload.js +28 -21
- package/dist/services/relations.js +93 -81
- package/dist/services/server.js +1 -0
- package/dist/services/shares.js +2 -1
- package/dist/services/specifications.js +1 -3
- package/dist/services/users.js +7 -2
- package/dist/types/files.d.ts +8 -0
- package/dist/utils/apply-query.js +38 -10
- package/dist/utils/apply-snapshot.d.ts +3 -1
- package/dist/utils/apply-snapshot.js +34 -5
- package/dist/utils/get-ast-from-query.js +15 -3
- package/dist/utils/get-column.d.ts +6 -5
- package/dist/utils/get-column.js +16 -8
- package/dist/utils/get-graphql-type.js +1 -0
- package/dist/utils/get-local-type.js +5 -0
- package/dist/utils/get-schema.d.ts +1 -1
- package/dist/utils/get-schema.js +18 -10
- package/dist/utils/jwt.js +1 -1
- package/dist/utils/reduce-schema.js +20 -12
- package/dist/utils/track.js +3 -2
- package/dist/utils/url.d.ts +1 -1
- package/dist/utils/url.js +1 -1
- package/dist/utils/validate-query.js +19 -15
- package/dist/utils/validate-storage.js +3 -1
- package/example.env +4 -0
- package/package.json +14 -13
|
@@ -15,9 +15,17 @@ fields:
|
|
|
15
15
|
|
|
16
16
|
- field: role
|
|
17
17
|
width: half
|
|
18
|
+
special: m2o
|
|
19
|
+
display: related-values
|
|
20
|
+
display_options:
|
|
21
|
+
template: '{{ name }}'
|
|
18
22
|
|
|
19
23
|
- field: user
|
|
20
24
|
width: half
|
|
25
|
+
special: m2o
|
|
26
|
+
display: related-values
|
|
27
|
+
display_options:
|
|
28
|
+
template: '{{ email }}'
|
|
21
29
|
|
|
22
30
|
- field: id
|
|
23
31
|
width: half
|
|
@@ -25,6 +33,12 @@ fields:
|
|
|
25
33
|
- field: bookmark
|
|
26
34
|
width: half
|
|
27
35
|
|
|
36
|
+
- field: icon
|
|
37
|
+
width: half
|
|
38
|
+
|
|
39
|
+
- field: color
|
|
40
|
+
width: half
|
|
41
|
+
|
|
28
42
|
- field: search
|
|
29
43
|
width: half
|
|
30
44
|
|
|
@@ -32,7 +32,17 @@ fields:
|
|
|
32
32
|
translations:
|
|
33
33
|
language: en-US
|
|
34
34
|
translations: Website
|
|
35
|
-
width:
|
|
35
|
+
width: half
|
|
36
|
+
|
|
37
|
+
- field: default_language
|
|
38
|
+
interface: system-language
|
|
39
|
+
options:
|
|
40
|
+
iconRight: language
|
|
41
|
+
placeholder: en-US
|
|
42
|
+
translations:
|
|
43
|
+
language: en-US
|
|
44
|
+
translations: Default Language
|
|
45
|
+
width: half
|
|
36
46
|
|
|
37
47
|
- field: branding_divider
|
|
38
48
|
interface: presentation-divider
|
|
@@ -380,4 +390,5 @@ fields:
|
|
|
380
390
|
placeholder: $t:fields.directus_settings.attribution_placeholder
|
|
381
391
|
|
|
382
392
|
- field: translation_strings
|
|
393
|
+
special: cast-json
|
|
383
394
|
hidden: true
|
|
@@ -72,6 +72,8 @@ fields:
|
|
|
72
72
|
- field: language
|
|
73
73
|
interface: system-language
|
|
74
74
|
width: half
|
|
75
|
+
options:
|
|
76
|
+
includeProjectDefault: true
|
|
75
77
|
|
|
76
78
|
- field: theme
|
|
77
79
|
interface: select-dropdown
|
|
@@ -152,6 +154,7 @@ fields:
|
|
|
152
154
|
- field: last_access
|
|
153
155
|
width: half
|
|
154
156
|
display: datetime
|
|
157
|
+
readonly: true
|
|
155
158
|
display_options:
|
|
156
159
|
relative: true
|
|
157
160
|
|
package/dist/env.js
CHANGED
|
@@ -14,7 +14,7 @@ const lodash_1 = require("lodash");
|
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const require_yaml_1 = require("./utils/require-yaml");
|
|
16
16
|
const utils_1 = require("@directus/shared/utils");
|
|
17
|
-
const acceptedEnvTypes = ['string', 'number', 'regex', 'array'];
|
|
17
|
+
const acceptedEnvTypes = ['string', 'number', 'regex', 'array', 'json'];
|
|
18
18
|
const defaults = {
|
|
19
19
|
CONFIG_PATH: path_1.default.resolve(process.cwd(), '.env'),
|
|
20
20
|
HOST: '0.0.0.0',
|
|
@@ -69,6 +69,7 @@ const defaults = {
|
|
|
69
69
|
SERVE_APP: true,
|
|
70
70
|
RELATIONAL_BATCH_SIZE: 25000,
|
|
71
71
|
EXPORT_BATCH_SIZE: 5000,
|
|
72
|
+
FILE_METADATA_ALLOW_LIST: 'ifd0.Make,ifd0.Model,exif.FNumber,exif.ExposureTime,exif.FocalLength,exif.ISO',
|
|
72
73
|
};
|
|
73
74
|
// Allows us to force certain environment variable into a type, instead of relying
|
|
74
75
|
// on the auto-parsed type in processValues. ref #3705
|
|
@@ -82,11 +83,12 @@ const typeMap = {
|
|
|
82
83
|
DB_PORT: 'number',
|
|
83
84
|
DB_EXCLUDE_TABLES: 'array',
|
|
84
85
|
IMPORT_IP_DENY_LIST: 'array',
|
|
86
|
+
FILE_METADATA_ALLOW_LIST: 'array',
|
|
85
87
|
};
|
|
86
88
|
let env = {
|
|
87
89
|
...defaults,
|
|
88
|
-
...getEnv(),
|
|
89
90
|
...process.env,
|
|
91
|
+
...getEnv(),
|
|
90
92
|
};
|
|
91
93
|
process.env = env;
|
|
92
94
|
env = processValues(env);
|
|
@@ -98,8 +100,8 @@ exports.default = env;
|
|
|
98
100
|
function refreshEnv() {
|
|
99
101
|
env = {
|
|
100
102
|
...defaults,
|
|
101
|
-
...getEnv(),
|
|
102
103
|
...process.env,
|
|
104
|
+
...getEnv(),
|
|
103
105
|
};
|
|
104
106
|
process.env = env;
|
|
105
107
|
env = processValues(env);
|
|
@@ -13,6 +13,7 @@ export * from './method-not-allowed';
|
|
|
13
13
|
export * from './range-not-satisfiable';
|
|
14
14
|
export * from './route-not-found';
|
|
15
15
|
export * from './service-unavailable';
|
|
16
|
+
export * from './token-expired';
|
|
16
17
|
export * from './unprocessable-entity';
|
|
17
18
|
export * from './unsupported-media-type';
|
|
18
19
|
export * from './user-suspended';
|
package/dist/exceptions/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __exportStar(require("./method-not-allowed"), exports);
|
|
|
25
25
|
__exportStar(require("./range-not-satisfiable"), exports);
|
|
26
26
|
__exportStar(require("./route-not-found"), exports);
|
|
27
27
|
__exportStar(require("./service-unavailable"), exports);
|
|
28
|
+
__exportStar(require("./token-expired"), exports);
|
|
28
29
|
__exportStar(require("./unprocessable-entity"), exports);
|
|
29
30
|
__exportStar(require("./unsupported-media-type"), exports);
|
|
30
31
|
__exportStar(require("./user-suspended"), exports);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenExpiredException = void 0;
|
|
4
|
+
const exceptions_1 = require("@directus/shared/exceptions");
|
|
5
|
+
class TokenExpiredException extends exceptions_1.BaseException {
|
|
6
|
+
constructor(message = 'Token expired.') {
|
|
7
|
+
super(message, 401, 'TOKEN_EXPIRED');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.TokenExpiredException = TokenExpiredException;
|
package/dist/logger.js
CHANGED
|
@@ -28,6 +28,7 @@ const pino_http_1 = __importStar(require("pino-http"));
|
|
|
28
28
|
const get_config_from_env_1 = require("./utils/get-config-from-env");
|
|
29
29
|
const url_1 = require("url");
|
|
30
30
|
const env_1 = __importDefault(require("./env"));
|
|
31
|
+
const utils_1 = require("@directus/shared/utils");
|
|
31
32
|
const pinoOptions = {
|
|
32
33
|
level: env_1.default.LOG_LEVEL || 'info',
|
|
33
34
|
redact: {
|
|
@@ -43,7 +44,7 @@ const loggerEnvConfig = (0, get_config_from_env_1.getConfigFromEnv)('LOGGER_', '
|
|
|
43
44
|
// Expose custom log levels into formatter function
|
|
44
45
|
if (loggerEnvConfig.levels) {
|
|
45
46
|
const customLogLevels = {};
|
|
46
|
-
for (const el of loggerEnvConfig.levels
|
|
47
|
+
for (const el of (0, utils_1.toArray)(loggerEnvConfig.levels)) {
|
|
47
48
|
const key_val = el.split(':');
|
|
48
49
|
customLogLevels[key_val[0].trim()] = key_val[1].trim();
|
|
49
50
|
}
|
package/dist/middleware/cache.js
CHANGED
|
@@ -19,6 +19,8 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
|
|
|
19
19
|
if (!cache)
|
|
20
20
|
return next();
|
|
21
21
|
if (((_a = req.headers['cache-control']) === null || _a === void 0 ? void 0 : _a.includes('no-store')) || ((_b = req.headers['Cache-Control']) === null || _b === void 0 ? void 0 : _b.includes('no-store'))) {
|
|
22
|
+
if (env_1.default.CACHE_STATUS_HEADER)
|
|
23
|
+
res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
|
|
22
24
|
return next();
|
|
23
25
|
}
|
|
24
26
|
const key = (0, get_cache_key_1.getCacheKey)(req);
|
|
@@ -28,6 +30,8 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
|
|
|
28
30
|
}
|
|
29
31
|
catch (err) {
|
|
30
32
|
logger_1.default.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`);
|
|
33
|
+
if (env_1.default.CACHE_STATUS_HEADER)
|
|
34
|
+
res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
|
|
31
35
|
return next();
|
|
32
36
|
}
|
|
33
37
|
if (cachedData) {
|
|
@@ -37,14 +41,20 @@ const checkCacheMiddleware = (0, async_handler_1.default)(async (req, res, next)
|
|
|
37
41
|
}
|
|
38
42
|
catch (err) {
|
|
39
43
|
logger_1.default.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`);
|
|
44
|
+
if (env_1.default.CACHE_STATUS_HEADER)
|
|
45
|
+
res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
|
|
40
46
|
return next();
|
|
41
47
|
}
|
|
42
48
|
const cacheTTL = cacheExpiryDate ? cacheExpiryDate - Date.now() : null;
|
|
43
49
|
res.setHeader('Cache-Control', (0, get_cache_headers_1.getCacheControlHeader)(req, cacheTTL));
|
|
44
50
|
res.setHeader('Vary', 'Origin, Cache-Control');
|
|
51
|
+
if (env_1.default.CACHE_STATUS_HEADER)
|
|
52
|
+
res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'HIT');
|
|
45
53
|
return res.json(cachedData);
|
|
46
54
|
}
|
|
47
55
|
else {
|
|
56
|
+
if (env_1.default.CACHE_STATUS_HEADER)
|
|
57
|
+
res.setHeader(`${env_1.default.CACHE_STATUS_HEADER}`, 'MISS');
|
|
48
58
|
return next();
|
|
49
59
|
}
|
|
50
60
|
});
|
|
@@ -16,6 +16,7 @@ const user_name_1 = require("../utils/user-name");
|
|
|
16
16
|
const lodash_1 = require("lodash");
|
|
17
17
|
const env_1 = __importDefault(require("../env"));
|
|
18
18
|
const uuid_validate_1 = __importDefault(require("uuid-validate"));
|
|
19
|
+
const url_1 = require("../utils/url");
|
|
19
20
|
class ActivityService extends items_1.ItemsService {
|
|
20
21
|
constructor(options) {
|
|
21
22
|
super('directus_activity', options);
|
|
@@ -70,7 +71,9 @@ ${(0, user_name_1.userName)(sender)} has mentioned you in a comment:
|
|
|
70
71
|
|
|
71
72
|
${comment}
|
|
72
73
|
|
|
73
|
-
<a href="${env_1.default.PUBLIC_URL
|
|
74
|
+
<a href="${new url_1.Url(env_1.default.PUBLIC_URL)
|
|
75
|
+
.addPath('admin', 'content', data.collection, data.item)
|
|
76
|
+
.toString()}">Click here to view.</a>
|
|
74
77
|
`;
|
|
75
78
|
await this.notificationsService.createOne({
|
|
76
79
|
recipient: userID,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { Accountability, PermissionsAction, SchemaOverview } from '@directus/shared/types';
|
|
1
2
|
import { Knex } from 'knex';
|
|
2
3
|
import { AbstractServiceOptions, AST, Item, PrimaryKey } from '../types';
|
|
3
|
-
import { PermissionsAction, Accountability, SchemaOverview } from '@directus/shared/types';
|
|
4
4
|
import { PayloadService } from './payload';
|
|
5
5
|
export declare class AuthorizationService {
|
|
6
6
|
knex: Knex;
|
|
@@ -4,14 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AuthorizationService = void 0;
|
|
7
|
+
const exceptions_1 = require("@directus/shared/exceptions");
|
|
8
|
+
const utils_1 = require("@directus/shared/utils");
|
|
7
9
|
const lodash_1 = require("lodash");
|
|
8
10
|
const database_1 = __importDefault(require("../database"));
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const utils_1 = require("@directus/shared/utils");
|
|
11
|
+
const exceptions_2 = require("../exceptions");
|
|
12
|
+
const strip_function_1 = require("../utils/strip-function");
|
|
12
13
|
const items_1 = require("./items");
|
|
13
14
|
const payload_1 = require("./payload");
|
|
14
|
-
const strip_function_1 = require("../utils/strip-function");
|
|
15
15
|
class AuthorizationService {
|
|
16
16
|
constructor(options) {
|
|
17
17
|
this.knex = options.knex || (0, database_1.default)();
|
|
@@ -32,10 +32,10 @@ class AuthorizationService {
|
|
|
32
32
|
// If the permissions don't match the collections, you don't have permission to read all of them
|
|
33
33
|
const uniqueCollectionsRequestedCount = (0, lodash_1.uniq)(collectionsRequested.map(({ collection }) => collection)).length;
|
|
34
34
|
if (uniqueCollectionsRequestedCount !== permissionsForCollections.length) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new exceptions_2.ForbiddenException();
|
|
36
36
|
}
|
|
37
37
|
validateFields(ast);
|
|
38
|
-
validateFilterPermissions(ast, this.schema, this.accountability);
|
|
38
|
+
validateFilterPermissions(ast, this.schema, action, this.accountability);
|
|
39
39
|
applyFilters(ast, this.accountability);
|
|
40
40
|
return ast;
|
|
41
41
|
/**
|
|
@@ -87,8 +87,10 @@ class AuthorizationService {
|
|
|
87
87
|
if (!aliasMap)
|
|
88
88
|
continue;
|
|
89
89
|
for (const column of Object.values(aliasMap)) {
|
|
90
|
+
if (column === '*')
|
|
91
|
+
continue;
|
|
90
92
|
if (allowedFields.includes(column) === false)
|
|
91
|
-
throw new
|
|
93
|
+
throw new exceptions_2.ForbiddenException();
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -101,65 +103,177 @@ class AuthorizationService {
|
|
|
101
103
|
continue;
|
|
102
104
|
const fieldKey = (0, strip_function_1.stripFunction)(childNode.name);
|
|
103
105
|
if (allowedFields.includes(fieldKey) === false) {
|
|
104
|
-
throw new
|
|
106
|
+
throw new exceptions_2.ForbiddenException();
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
|
-
function validateFilterPermissions(ast, schema, accountability) {
|
|
111
|
+
function validateFilterPermissions(ast, schema, action, accountability) {
|
|
110
112
|
var _a, _b, _c, _d, _e;
|
|
113
|
+
let requiredFieldPermissions = {};
|
|
111
114
|
if (ast.type !== 'field') {
|
|
112
115
|
if (ast.type === 'a2o') {
|
|
113
116
|
for (const collection of Object.keys(ast.children)) {
|
|
114
|
-
|
|
117
|
+
requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, extractRequiredFieldPermissions(collection, (_c = (_b = (_a = ast.query) === null || _a === void 0 ? void 0 : _a[collection]) === null || _b === void 0 ? void 0 : _b.filter) !== null && _c !== void 0 ? _c : {}));
|
|
115
118
|
for (const child of ast.children[collection]) {
|
|
116
|
-
validateFilterPermissions(child, schema, accountability);
|
|
119
|
+
const childPermissions = validateFilterPermissions(child, schema, action, accountability);
|
|
120
|
+
if (Object.keys(childPermissions).length > 0) {
|
|
121
|
+
//Only add relational field if deep child has a filter
|
|
122
|
+
if (child.type !== 'field') {
|
|
123
|
+
(requiredFieldPermissions[collection] || (requiredFieldPermissions[collection] = new Set())).add(child.fieldKey);
|
|
124
|
+
}
|
|
125
|
+
requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, childPermissions);
|
|
126
|
+
}
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
else {
|
|
121
|
-
|
|
131
|
+
requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, extractRequiredFieldPermissions(ast.name, (_e = (_d = ast.query) === null || _d === void 0 ? void 0 : _d.filter) !== null && _e !== void 0 ? _e : {}));
|
|
122
132
|
for (const child of ast.children) {
|
|
123
|
-
validateFilterPermissions(child, schema, accountability);
|
|
133
|
+
const childPermissions = validateFilterPermissions(child, schema, action, accountability);
|
|
134
|
+
if (Object.keys(childPermissions).length > 0) {
|
|
135
|
+
// Only add relational field if deep child has a filter
|
|
136
|
+
if (child.type !== 'field') {
|
|
137
|
+
(requiredFieldPermissions[ast.name] || (requiredFieldPermissions[ast.name] = new Set())).add(child.fieldKey);
|
|
138
|
+
}
|
|
139
|
+
requiredFieldPermissions = mergeRequiredFieldPermissions(requiredFieldPermissions, childPermissions);
|
|
140
|
+
}
|
|
124
141
|
}
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
if (ast.type === 'root') {
|
|
145
|
+
// Validate all required permissions once at the root level
|
|
146
|
+
checkFieldPermissions(ast.name, schema, action, requiredFieldPermissions);
|
|
147
|
+
}
|
|
148
|
+
return requiredFieldPermissions;
|
|
149
|
+
function extractRequiredFieldPermissions(collection, filter, parentCollection, parentField) {
|
|
150
|
+
return (0, lodash_1.reduce)(filter, function (result, filterValue, filterKey) {
|
|
151
|
+
if (filterKey.startsWith('_')) {
|
|
152
|
+
if (filterKey === '_and' || filterKey === '_or') {
|
|
153
|
+
if ((0, lodash_1.isArray)(filterValue)) {
|
|
154
|
+
for (const filter of filterValue) {
|
|
155
|
+
const requiredPermissions = extractRequiredFieldPermissions(collection, filter, parentCollection, parentField);
|
|
156
|
+
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
|
157
|
+
}
|
|
139
158
|
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
// Filter value is not a filter, so we should skip it
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
// m2a filter in the form of `item:collection`
|
|
165
|
+
else if (filterKey.includes(':')) {
|
|
166
|
+
const [field, collectionScope] = filterKey.split(':');
|
|
167
|
+
if (collection) {
|
|
168
|
+
// Add the `item` field to the required permissions
|
|
169
|
+
(result[collection] || (result[collection] = new Set())).add(field);
|
|
170
|
+
// Add the `collection` field to the required permissions
|
|
171
|
+
result[collection].add('collection');
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const relation = schema.relations.find((relation) => {
|
|
175
|
+
var _a;
|
|
176
|
+
return ((relation.collection === parentCollection && relation.field === parentField) ||
|
|
177
|
+
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === parentField));
|
|
178
|
+
});
|
|
179
|
+
// Filter key not found in parent collection
|
|
180
|
+
if (!relation)
|
|
181
|
+
throw new exceptions_2.ForbiddenException();
|
|
182
|
+
const relatedCollectionName = relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
|
|
183
|
+
// Add the `item` field to the required permissions
|
|
184
|
+
(result[relatedCollectionName] || (result[relatedCollectionName] = new Set())).add(field);
|
|
185
|
+
// Add the `collection` field to the required permissions
|
|
186
|
+
result[relatedCollectionName].add('collection');
|
|
140
187
|
}
|
|
188
|
+
// Continue to parse the filter for nested `collection` afresh
|
|
189
|
+
const requiredPermissions = extractRequiredFieldPermissions(collectionScope, filterValue);
|
|
190
|
+
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
|
141
191
|
}
|
|
142
192
|
else {
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
allowedFields.includes(key) === false) {
|
|
146
|
-
throw new exceptions_1.ForbiddenException();
|
|
193
|
+
if (collection) {
|
|
194
|
+
(result[collection] || (result[collection] = new Set())).add(filterKey);
|
|
147
195
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
(relation.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (relation
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
else {
|
|
197
|
+
const relation = schema.relations.find((relation) => {
|
|
198
|
+
var _a;
|
|
199
|
+
return ((relation.collection === parentCollection && relation.field === parentField) ||
|
|
200
|
+
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === parentField));
|
|
201
|
+
});
|
|
202
|
+
// Filter key not found in parent collection
|
|
203
|
+
if (!relation)
|
|
204
|
+
throw new exceptions_2.ForbiddenException();
|
|
205
|
+
parentCollection =
|
|
206
|
+
relation.related_collection === parentCollection ? relation.collection : relation.related_collection;
|
|
207
|
+
(result[parentCollection] || (result[parentCollection] = new Set())).add(filterKey);
|
|
208
|
+
}
|
|
209
|
+
if (typeof filterValue === 'object') {
|
|
210
|
+
// Parent collection is undefined when we process the top level filter
|
|
211
|
+
if (!parentCollection)
|
|
212
|
+
parentCollection = collection;
|
|
213
|
+
for (const [childFilterKey, childFilterValue] of Object.entries(filterValue)) {
|
|
214
|
+
if (childFilterKey.startsWith('_')) {
|
|
215
|
+
if (childFilterKey === '_and' || childFilterKey === '_or') {
|
|
216
|
+
if ((0, lodash_1.isArray)(childFilterValue)) {
|
|
217
|
+
for (const filter of childFilterValue) {
|
|
218
|
+
const requiredPermissions = extractRequiredFieldPermissions('', filter, parentCollection, filterKey);
|
|
219
|
+
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const requiredPermissions = extractRequiredFieldPermissions('', filterValue, parentCollection, filterKey);
|
|
226
|
+
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
|
227
|
+
}
|
|
160
228
|
}
|
|
161
229
|
}
|
|
162
230
|
}
|
|
231
|
+
return result;
|
|
232
|
+
}, {});
|
|
233
|
+
}
|
|
234
|
+
function mergeRequiredFieldPermissions(current, child) {
|
|
235
|
+
for (const collection of Object.keys(child)) {
|
|
236
|
+
if (!current[collection]) {
|
|
237
|
+
current[collection] = child[collection];
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
current[collection] = new Set([...current[collection], ...child[collection]]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
function checkFieldPermissions(rootCollection, schema, action, requiredPermissions) {
|
|
246
|
+
var _a, _b;
|
|
247
|
+
if ((accountability === null || accountability === void 0 ? void 0 : accountability.admin) === true)
|
|
248
|
+
return;
|
|
249
|
+
for (const collection of Object.keys(requiredPermissions)) {
|
|
250
|
+
const permission = (_a = accountability === null || accountability === void 0 ? void 0 : accountability.permissions) === null || _a === void 0 ? void 0 : _a.find((permission) => permission.collection === collection && permission.action === 'read');
|
|
251
|
+
let allowedFields;
|
|
252
|
+
// Allow the filtering of top level ID for actions such as update and delete
|
|
253
|
+
if (action !== 'read' && collection === rootCollection) {
|
|
254
|
+
const actionPermission = (_b = accountability === null || accountability === void 0 ? void 0 : accountability.permissions) === null || _b === void 0 ? void 0 : _b.find((permission) => permission.collection === collection && permission.action === action);
|
|
255
|
+
if (!actionPermission || !actionPermission.fields) {
|
|
256
|
+
throw new exceptions_2.ForbiddenException();
|
|
257
|
+
}
|
|
258
|
+
allowedFields = (permission === null || permission === void 0 ? void 0 : permission.fields)
|
|
259
|
+
? [...permission.fields, schema.collections[collection].primary]
|
|
260
|
+
: [schema.collections[collection].primary];
|
|
261
|
+
}
|
|
262
|
+
else if (!permission || !permission.fields) {
|
|
263
|
+
throw new exceptions_2.ForbiddenException();
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
allowedFields = permission.fields;
|
|
267
|
+
}
|
|
268
|
+
if (allowedFields.includes('*'))
|
|
269
|
+
continue;
|
|
270
|
+
// Allow legacy permissions with an empty fields array, where id can be accessed
|
|
271
|
+
if (allowedFields.length === 0)
|
|
272
|
+
allowedFields.push(schema.collections[collection].primary);
|
|
273
|
+
for (const field of requiredPermissions[collection]) {
|
|
274
|
+
if (!allowedFields.includes(field))
|
|
275
|
+
throw new exceptions_2.ForbiddenException();
|
|
276
|
+
}
|
|
163
277
|
}
|
|
164
278
|
}
|
|
165
279
|
}
|
|
@@ -222,20 +336,24 @@ class AuthorizationService {
|
|
|
222
336
|
return permission.collection === collection && permission.action === action;
|
|
223
337
|
});
|
|
224
338
|
if (!permission)
|
|
225
|
-
throw new
|
|
339
|
+
throw new exceptions_2.ForbiddenException();
|
|
226
340
|
// Check if you have permission to access the fields you're trying to access
|
|
227
341
|
const allowedFields = permission.fields || [];
|
|
228
342
|
if (allowedFields.includes('*') === false) {
|
|
229
343
|
const keysInData = Object.keys(payload);
|
|
230
344
|
const invalidKeys = keysInData.filter((fieldKey) => allowedFields.includes(fieldKey) === false);
|
|
231
345
|
if (invalidKeys.length > 0) {
|
|
232
|
-
throw new
|
|
346
|
+
throw new exceptions_2.ForbiddenException();
|
|
233
347
|
}
|
|
234
348
|
}
|
|
235
349
|
}
|
|
236
350
|
const preset = (_e = permission.presets) !== null && _e !== void 0 ? _e : {};
|
|
237
351
|
const payloadWithPresets = (0, lodash_1.merge)({}, preset, payload);
|
|
352
|
+
const fieldValidationRules = Object.values(this.schema.collections[collection].fields)
|
|
353
|
+
.map((field) => field.validation)
|
|
354
|
+
.filter((v) => v);
|
|
238
355
|
const hasValidationRules = (0, lodash_1.isNil)(permission.validation) === false && Object.keys((_f = permission.validation) !== null && _f !== void 0 ? _f : {}).length > 0;
|
|
356
|
+
const hasFieldValidationRules = fieldValidationRules && fieldValidationRules.length > 0;
|
|
239
357
|
const requiredColumns = [];
|
|
240
358
|
for (const field of Object.values(this.schema.collections[collection].fields)) {
|
|
241
359
|
const specials = (_g = field === null || field === void 0 ? void 0 : field.special) !== null && _g !== void 0 ? _g : [];
|
|
@@ -245,7 +363,7 @@ class AuthorizationService {
|
|
|
245
363
|
requiredColumns.push(field);
|
|
246
364
|
}
|
|
247
365
|
}
|
|
248
|
-
if (hasValidationRules === false && requiredColumns.length === 0) {
|
|
366
|
+
if (hasValidationRules === false && hasFieldValidationRules === false && requiredColumns.length === 0) {
|
|
249
367
|
return payloadWithPresets;
|
|
250
368
|
}
|
|
251
369
|
if (requiredColumns.length > 0) {
|
|
@@ -265,8 +383,16 @@ class AuthorizationService {
|
|
|
265
383
|
});
|
|
266
384
|
}
|
|
267
385
|
}
|
|
386
|
+
if (hasFieldValidationRules) {
|
|
387
|
+
if (permission.validation && Object.keys(permission.validation).length > 0) {
|
|
388
|
+
permission.validation = { _and: [permission.validation, ...fieldValidationRules] };
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
permission.validation = { _and: fieldValidationRules };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
268
394
|
const validationErrors = [];
|
|
269
|
-
validationErrors.push(...(0, lodash_1.flatten)((0, utils_1.validatePayload)(permission.validation, payloadWithPresets).map((error) => error.details.map((details) => new
|
|
395
|
+
validationErrors.push(...(0, lodash_1.flatten)((0, utils_1.validatePayload)(permission.validation, payloadWithPresets).map((error) => error.details.map((details) => new exceptions_1.FailedValidationException(details)))));
|
|
270
396
|
if (validationErrors.length > 0)
|
|
271
397
|
throw validationErrors;
|
|
272
398
|
return payloadWithPresets;
|
|
@@ -286,14 +412,14 @@ class AuthorizationService {
|
|
|
286
412
|
if (Array.isArray(pk)) {
|
|
287
413
|
const result = await itemsService.readMany(pk, { ...query, limit: pk.length }, { permissionsAction: action });
|
|
288
414
|
if (!result)
|
|
289
|
-
throw new
|
|
415
|
+
throw new exceptions_2.ForbiddenException();
|
|
290
416
|
if (result.length !== pk.length)
|
|
291
|
-
throw new
|
|
417
|
+
throw new exceptions_2.ForbiddenException();
|
|
292
418
|
}
|
|
293
419
|
else {
|
|
294
420
|
const result = await itemsService.readOne(pk, query, { permissionsAction: action });
|
|
295
421
|
if (!result)
|
|
296
|
-
throw new
|
|
422
|
+
throw new exceptions_2.ForbiddenException();
|
|
297
423
|
}
|
|
298
424
|
}
|
|
299
425
|
}
|
|
@@ -4,6 +4,7 @@ import Keyv from 'keyv';
|
|
|
4
4
|
import { AbstractServiceOptions, Collection, CollectionMeta, MutationOptions } from '../types';
|
|
5
5
|
import { Accountability, RawField, SchemaOverview } from '@directus/shared/types';
|
|
6
6
|
import { Table } from 'knex-schema-inspector/dist/types/table';
|
|
7
|
+
import { Helpers } from '../database/helpers';
|
|
7
8
|
export declare type RawCollection = {
|
|
8
9
|
collection: string;
|
|
9
10
|
fields?: RawField[];
|
|
@@ -12,6 +13,7 @@ export declare type RawCollection = {
|
|
|
12
13
|
};
|
|
13
14
|
export declare class CollectionsService {
|
|
14
15
|
knex: Knex;
|
|
16
|
+
helpers: Helpers;
|
|
15
17
|
accountability: Accountability | null;
|
|
16
18
|
schemaInspector: ReturnType<typeof SchemaInspector>;
|
|
17
19
|
schema: SchemaOverview;
|