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.
Files changed (103) hide show
  1. package/dist/__mocks__/cache.d.ts +5 -0
  2. package/dist/__mocks__/cache.js +7 -0
  3. package/dist/auth/drivers/ldap.js +10 -11
  4. package/dist/auth/drivers/oauth2.js +9 -4
  5. package/dist/auth/drivers/openid.js +7 -4
  6. package/dist/cache.js +2 -2
  7. package/dist/cli/commands/schema/apply.js +9 -3
  8. package/dist/controllers/assets.js +5 -0
  9. package/dist/controllers/files.d.ts +2 -0
  10. package/dist/controllers/files.js +13 -5
  11. package/dist/database/helpers/date/dialects/default.d.ts +3 -0
  12. package/dist/database/helpers/date/dialects/default.js +7 -0
  13. package/dist/database/helpers/date/dialects/mssql.d.ts +1 -9
  14. package/dist/database/helpers/date/dialects/mssql.js +4 -23
  15. package/dist/database/helpers/date/dialects/mysql.d.ts +2 -9
  16. package/dist/database/helpers/date/dialects/mysql.js +7 -22
  17. package/dist/database/helpers/date/dialects/oracle.d.ts +1 -9
  18. package/dist/database/helpers/date/dialects/oracle.js +7 -23
  19. package/dist/database/helpers/date/dialects/sqlite.d.ts +1 -9
  20. package/dist/database/helpers/date/dialects/sqlite.js +8 -24
  21. package/dist/database/helpers/date/index.d.ts +4 -4
  22. package/dist/database/helpers/date/index.js +9 -9
  23. package/dist/database/helpers/date/types.d.ts +3 -9
  24. package/dist/database/helpers/date/types.js +10 -0
  25. package/dist/database/helpers/fn/dialects/mssql.d.ts +13 -0
  26. package/dist/database/helpers/fn/dialects/mssql.js +42 -0
  27. package/dist/database/helpers/{date/dialects/postgres.d.ts → fn/dialects/mysql.d.ts} +3 -2
  28. package/dist/database/helpers/fn/dialects/mysql.js +42 -0
  29. package/dist/database/helpers/fn/dialects/oracle.d.ts +13 -0
  30. package/dist/database/helpers/fn/dialects/oracle.js +42 -0
  31. package/dist/database/helpers/fn/dialects/postgres.d.ts +13 -0
  32. package/dist/database/helpers/{date → fn}/dialects/postgres.js +18 -3
  33. package/dist/database/helpers/fn/dialects/sqlite.d.ts +13 -0
  34. package/dist/database/helpers/fn/dialects/sqlite.js +42 -0
  35. package/dist/database/helpers/fn/index.d.ts +7 -0
  36. package/dist/database/helpers/fn/index.js +17 -0
  37. package/dist/database/helpers/fn/types.d.ts +18 -0
  38. package/dist/database/helpers/fn/types.js +27 -0
  39. package/dist/database/helpers/index.d.ts +4 -1
  40. package/dist/database/helpers/index.js +7 -1
  41. package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.d.ts +3 -0
  42. package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.js +17 -0
  43. package/dist/database/migrations/20220322A-rename-field-typecast-flags.js +6 -2
  44. package/dist/database/migrations/20220323A-add-field-validation.d.ts +3 -0
  45. package/dist/database/migrations/20220323A-add-field-validation.js +17 -0
  46. package/dist/database/migrations/20220325A-fix-typecast-flags.d.ts +3 -0
  47. package/dist/database/migrations/20220325A-fix-typecast-flags.js +49 -0
  48. package/dist/database/migrations/20220325B-add-default-language.d.ts +3 -0
  49. package/dist/database/migrations/20220325B-add-default-language.js +28 -0
  50. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.d.ts +3 -0
  51. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +22 -0
  52. package/dist/database/run-ast.js +7 -5
  53. package/dist/database/system-data/fields/activity.yaml +4 -4
  54. package/dist/database/system-data/fields/collections.yaml +1 -1
  55. package/dist/database/system-data/fields/fields.yaml +9 -0
  56. package/dist/database/system-data/fields/presets.yaml +14 -0
  57. package/dist/database/system-data/fields/settings.yaml +12 -1
  58. package/dist/database/system-data/fields/users.yaml +3 -0
  59. package/dist/env.js +5 -3
  60. package/dist/exceptions/index.d.ts +1 -0
  61. package/dist/exceptions/index.js +1 -0
  62. package/dist/exceptions/token-expired.d.ts +4 -0
  63. package/dist/exceptions/token-expired.js +10 -0
  64. package/dist/logger.js +2 -1
  65. package/dist/middleware/cache.js +10 -0
  66. package/dist/services/activity.js +4 -1
  67. package/dist/services/authorization.d.ts +1 -1
  68. package/dist/services/authorization.js +174 -48
  69. package/dist/services/collections.d.ts +2 -0
  70. package/dist/services/collections.js +232 -198
  71. package/dist/services/fields.js +210 -174
  72. package/dist/services/files.d.ts +5 -1
  73. package/dist/services/files.js +59 -40
  74. package/dist/services/graphql.d.ts +2 -3
  75. package/dist/services/graphql.js +53 -10
  76. package/dist/services/items.js +5 -3
  77. package/dist/services/payload.d.ts +2 -1
  78. package/dist/services/payload.js +28 -21
  79. package/dist/services/relations.js +93 -81
  80. package/dist/services/server.js +1 -0
  81. package/dist/services/shares.js +2 -1
  82. package/dist/services/specifications.js +1 -3
  83. package/dist/services/users.js +7 -2
  84. package/dist/types/files.d.ts +8 -0
  85. package/dist/utils/apply-query.js +38 -10
  86. package/dist/utils/apply-snapshot.d.ts +3 -1
  87. package/dist/utils/apply-snapshot.js +34 -5
  88. package/dist/utils/get-ast-from-query.js +15 -3
  89. package/dist/utils/get-column.d.ts +6 -5
  90. package/dist/utils/get-column.js +16 -8
  91. package/dist/utils/get-graphql-type.js +1 -0
  92. package/dist/utils/get-local-type.js +5 -0
  93. package/dist/utils/get-schema.d.ts +1 -1
  94. package/dist/utils/get-schema.js +18 -10
  95. package/dist/utils/jwt.js +1 -1
  96. package/dist/utils/reduce-schema.js +20 -12
  97. package/dist/utils/track.js +3 -2
  98. package/dist/utils/url.d.ts +1 -1
  99. package/dist/utils/url.js +1 -1
  100. package/dist/utils/validate-query.js +19 -15
  101. package/dist/utils/validate-storage.js +3 -1
  102. package/example.env +4 -0
  103. package/package.json +14 -13
@@ -84,3 +84,12 @@ fields:
84
84
  field: conditions
85
85
  hidden: true
86
86
  special: cast-json
87
+
88
+ - collection: directus_fields
89
+ field: validation
90
+ hidden: true
91
+ special: cast-json
92
+
93
+ - collection: directus_fields
94
+ field: validation_message
95
+ hidden: true
@@ -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: full
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';
@@ -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,4 @@
1
+ import { BaseException } from '@directus/shared/exceptions';
2
+ export declare class TokenExpiredException extends BaseException {
3
+ constructor(message?: string);
4
+ }
@@ -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.split(',')) {
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
  }
@@ -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}/admin/content/${data.collection}/${data.item}">Click here to view.</a>
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 exceptions_1 = require("../exceptions");
10
- const exceptions_2 = require("@directus/shared/exceptions");
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 exceptions_1.ForbiddenException();
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 exceptions_1.ForbiddenException();
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 exceptions_1.ForbiddenException();
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
- checkFilter(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 : {});
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
- checkFilter(ast.name, (_e = (_d = ast.query) === null || _d === void 0 ? void 0 : _d.filter) !== null && _e !== void 0 ? _e : {});
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
- function checkFilter(collection, filter) {
128
- var _a;
129
- const permissions = (_a = accountability === null || accountability === void 0 ? void 0 : accountability.permissions) === null || _a === void 0 ? void 0 : _a.find((permission) => permission.collection === collection);
130
- if (!permissions)
131
- throw new exceptions_1.ForbiddenException();
132
- const allowedFields = permissions.fields || [];
133
- for (const [key, value] of Object.entries(filter)) {
134
- if (key.startsWith('_')) {
135
- // Continue checking for _and and _or
136
- if ((0, lodash_1.isArray)(value)) {
137
- for (const val of value) {
138
- checkFilter(collection, val);
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 (allowedFields.length !== 0 &&
144
- allowedFields.includes('*') === false &&
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
- const relation = schema.relations.find((relation) => {
149
- var _a;
150
- return ((relation.collection === collection && relation.field === key) ||
151
- (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === key));
152
- });
153
- // Field is a relation
154
- if (relation) {
155
- if (relation.related_collection === collection) {
156
- checkFilter(relation.collection, value);
157
- }
158
- else {
159
- checkFilter(relation.related_collection, value);
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 exceptions_1.ForbiddenException();
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 exceptions_1.ForbiddenException();
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 exceptions_2.FailedValidationException(details)))));
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 exceptions_1.ForbiddenException();
415
+ throw new exceptions_2.ForbiddenException();
290
416
  if (result.length !== pk.length)
291
- throw new exceptions_1.ForbiddenException();
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 exceptions_1.ForbiddenException();
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;