@directus/api 16.0.0 → 17.0.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 (92) hide show
  1. package/dist/controllers/items.js +8 -7
  2. package/dist/controllers/permissions.js +11 -2
  3. package/dist/controllers/utils.js +13 -32
  4. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
  5. package/dist/flows.js +2 -1
  6. package/dist/middleware/collection-exists.js +6 -6
  7. package/dist/middleware/respond.js +1 -1
  8. package/dist/operations/request/index.js +5 -5
  9. package/dist/request/agent-with-ip-validation.d.ts +11 -0
  10. package/dist/request/agent-with-ip-validation.js +34 -0
  11. package/dist/request/index.js +6 -5
  12. package/dist/request/is-denied-ip.d.ts +1 -0
  13. package/dist/request/{validate-ip.js → is-denied-ip.js} +10 -12
  14. package/dist/services/assets.js +1 -3
  15. package/dist/services/authorization.d.ts +1 -1
  16. package/dist/services/authorization.js +15 -3
  17. package/dist/services/collections.d.ts +3 -2
  18. package/dist/services/collections.js +1 -1
  19. package/dist/services/fields.js +2 -1
  20. package/dist/services/files.js +4 -3
  21. package/dist/services/graphql/index.js +3 -2
  22. package/dist/services/{import-export/index.d.ts → import-export.d.ts} +1 -1
  23. package/dist/services/{import-export/index.js → import-export.js} +12 -11
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/index.js +1 -1
  26. package/dist/services/items.js +2 -1
  27. package/dist/services/permissions.d.ts +3 -2
  28. package/dist/services/permissions.js +77 -2
  29. package/dist/services/relations.js +1 -1
  30. package/dist/services/roles.js +83 -15
  31. package/dist/services/server.js +2 -1
  32. package/dist/services/specifications.js +4 -3
  33. package/dist/services/utils.js +1 -1
  34. package/dist/telemetry/utils/get-user-item-count.js +2 -1
  35. package/dist/types/collection.d.ts +2 -13
  36. package/dist/types/items.d.ts +4 -12
  37. package/dist/types/items.js +0 -4
  38. package/dist/utils/get-field-system-rows.d.ts +2 -0
  39. package/dist/utils/get-field-system-rows.js +17 -0
  40. package/dist/utils/get-permissions.js +1 -1
  41. package/dist/utils/get-schema.js +3 -2
  42. package/dist/utils/merge-permissions-for-share.js +1 -1
  43. package/dist/utils/should-skip-cache.js +1 -1
  44. package/dist/websocket/handlers/items.js +2 -1
  45. package/dist/websocket/messages.d.ts +18 -18
  46. package/package.json +36 -35
  47. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -107
  48. package/dist/database/system-data/app-access-permissions/index.d.ts +0 -3
  49. package/dist/database/system-data/app-access-permissions/index.js +0 -17
  50. package/dist/database/system-data/app-access-permissions/schema-access-permissions.yaml +0 -17
  51. package/dist/database/system-data/collections/collections.yaml +0 -103
  52. package/dist/database/system-data/collections/index.d.ts +0 -2
  53. package/dist/database/system-data/collections/index.js +0 -9
  54. package/dist/database/system-data/fields/_defaults.yaml +0 -16
  55. package/dist/database/system-data/fields/activity.yaml +0 -83
  56. package/dist/database/system-data/fields/collections.yaml +0 -249
  57. package/dist/database/system-data/fields/dashboards.yaml +0 -20
  58. package/dist/database/system-data/fields/extensions.yaml +0 -10
  59. package/dist/database/system-data/fields/fields.yaml +0 -104
  60. package/dist/database/system-data/fields/files.yaml +0 -160
  61. package/dist/database/system-data/fields/flows.yaml +0 -26
  62. package/dist/database/system-data/fields/folders.yaml +0 -14
  63. package/dist/database/system-data/fields/index.d.ts +0 -2
  64. package/dist/database/system-data/fields/index.js +0 -33
  65. package/dist/database/system-data/fields/migrations.yaml +0 -10
  66. package/dist/database/system-data/fields/notifications.yaml +0 -15
  67. package/dist/database/system-data/fields/operations.yaml +0 -23
  68. package/dist/database/system-data/fields/panels.yaml +0 -29
  69. package/dist/database/system-data/fields/permissions.yaml +0 -37
  70. package/dist/database/system-data/fields/presets.yaml +0 -56
  71. package/dist/database/system-data/fields/relations.yaml +0 -34
  72. package/dist/database/system-data/fields/revisions.yaml +0 -30
  73. package/dist/database/system-data/fields/roles.yaml +0 -61
  74. package/dist/database/system-data/fields/sessions.yaml +0 -16
  75. package/dist/database/system-data/fields/settings.yaml +0 -471
  76. package/dist/database/system-data/fields/shares.yaml +0 -83
  77. package/dist/database/system-data/fields/translations.yaml +0 -27
  78. package/dist/database/system-data/fields/users.yaml +0 -224
  79. package/dist/database/system-data/fields/versions.yaml +0 -38
  80. package/dist/database/system-data/fields/webhooks.yaml +0 -141
  81. package/dist/database/system-data/relations/index.d.ts +0 -2
  82. package/dist/database/system-data/relations/index.js +0 -9
  83. package/dist/database/system-data/relations/relations.yaml +0 -197
  84. package/dist/request/request-interceptor.d.ts +0 -2
  85. package/dist/request/request-interceptor.js +0 -28
  86. package/dist/request/response-interceptor.d.ts +0 -2
  87. package/dist/request/response-interceptor.js +0 -5
  88. package/dist/request/validate-ip.d.ts +0 -1
  89. package/dist/services/import-export/import-worker.d.ts +0 -9
  90. package/dist/services/import-export/import-worker.js +0 -9
  91. package/dist/worker-pool.d.ts +0 -2
  92. package/dist/worker-pool.js +0 -19
@@ -8,9 +8,10 @@ import { ItemsService } from '../services/items.js';
8
8
  import { MetaService } from '../services/meta.js';
9
9
  import asyncHandler from '../utils/async-handler.js';
10
10
  import { sanitizeQuery } from '../utils/sanitize-query.js';
11
+ import { isSystemCollection } from '@directus/system-data';
11
12
  const router = express.Router();
12
13
  router.post('/:collection', collectionExists, asyncHandler(async (req, res, next) => {
13
- if (req.params['collection'].startsWith('directus_'))
14
+ if (isSystemCollection(req.params['collection']))
14
15
  throw new ForbiddenError();
15
16
  if (req.singleton) {
16
17
  throw new RouteNotFoundError({ path: req.path });
@@ -47,7 +48,7 @@ router.post('/:collection', collectionExists, asyncHandler(async (req, res, next
47
48
  return next();
48
49
  }), respond);
49
50
  const readHandler = asyncHandler(async (req, res, next) => {
50
- if (req.params['collection'].startsWith('directus_'))
51
+ if (isSystemCollection(req.params['collection']))
51
52
  throw new ForbiddenError();
52
53
  const service = new ItemsService(req.collection, {
53
54
  accountability: req.accountability,
@@ -77,7 +78,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
77
78
  router.search('/:collection', collectionExists, validateBatch('read'), readHandler, respond);
78
79
  router.get('/:collection', collectionExists, readHandler, respond);
79
80
  router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
80
- if (req.params['collection'].startsWith('directus_'))
81
+ if (isSystemCollection(req.params['collection']))
81
82
  throw new ForbiddenError();
82
83
  const service = new ItemsService(req.collection, {
83
84
  accountability: req.accountability,
@@ -90,7 +91,7 @@ router.get('/:collection/:pk', collectionExists, asyncHandler(async (req, res, n
90
91
  return next();
91
92
  }), respond);
92
93
  router.patch('/:collection', collectionExists, validateBatch('update'), asyncHandler(async (req, res, next) => {
93
- if (req.params['collection'].startsWith('directus_'))
94
+ if (isSystemCollection(req.params['collection']))
94
95
  throw new ForbiddenError();
95
96
  const service = new ItemsService(req.collection, {
96
97
  accountability: req.accountability,
@@ -126,7 +127,7 @@ router.patch('/:collection', collectionExists, validateBatch('update'), asyncHan
126
127
  return next();
127
128
  }), respond);
128
129
  router.patch('/:collection/:pk', collectionExists, asyncHandler(async (req, res, next) => {
129
- if (req.params['collection'].startsWith('directus_'))
130
+ if (isSystemCollection(req.params['collection']))
130
131
  throw new ForbiddenError();
131
132
  if (req.singleton) {
132
133
  throw new RouteNotFoundError({ path: req.path });
@@ -149,7 +150,7 @@ router.patch('/:collection/:pk', collectionExists, asyncHandler(async (req, res,
149
150
  return next();
150
151
  }), respond);
151
152
  router.delete('/:collection', collectionExists, validateBatch('delete'), asyncHandler(async (req, _res, next) => {
152
- if (req.params['collection'].startsWith('directus_'))
153
+ if (isSystemCollection(req.params['collection']))
153
154
  throw new ForbiddenError();
154
155
  const service = new ItemsService(req.collection, {
155
156
  accountability: req.accountability,
@@ -168,7 +169,7 @@ router.delete('/:collection', collectionExists, validateBatch('delete'), asyncHa
168
169
  return next();
169
170
  }), respond);
170
171
  router.delete('/:collection/:pk', collectionExists, asyncHandler(async (req, _res, next) => {
171
- if (req.params['collection'].startsWith('directus_'))
172
+ if (isSystemCollection(req.params['collection']))
172
173
  throw new ForbiddenError();
173
174
  const service = new ItemsService(req.collection, {
174
175
  accountability: req.accountability,
@@ -1,6 +1,5 @@
1
- import { isDirectusError } from '@directus/errors';
1
+ import { ErrorCode, isDirectusError } from '@directus/errors';
2
2
  import express from 'express';
3
- import { ErrorCode } from '@directus/errors';
4
3
  import { respond } from '../middleware/respond.js';
5
4
  import useCollection from '../middleware/use-collection.js';
6
5
  import { validateBatch } from '../middleware/validate-batch.js';
@@ -152,4 +151,14 @@ router.delete('/:pk', asyncHandler(async (req, _res, next) => {
152
151
  await service.deleteOne(req.params['pk']);
153
152
  return next();
154
153
  }), respond);
154
+ router.get('/me/:collection/:pk?', asyncHandler(async (req, res, next) => {
155
+ const { collection, pk } = req.params;
156
+ const service = new PermissionsService({
157
+ accountability: req.accountability,
158
+ schema: req.schema,
159
+ });
160
+ const itemPermissions = await service.getItemPermissions(collection, pk);
161
+ res.locals['payload'] = { data: itemPermissions };
162
+ return next();
163
+ }), respond);
155
164
  export default router;
@@ -1,13 +1,11 @@
1
+ import { InvalidPayloadError, InvalidQueryError, UnsupportedMediaTypeError } from '@directus/errors';
1
2
  import argon2 from 'argon2';
2
3
  import Busboy from 'busboy';
3
4
  import { Router } from 'express';
4
5
  import Joi from 'joi';
5
- import fs from 'node:fs';
6
- import { createRequire } from 'node:module';
7
- import { InvalidPayloadError, InvalidQueryError, UnsupportedMediaTypeError } from '@directus/errors';
8
6
  import collectionExists from '../middleware/collection-exists.js';
9
7
  import { respond } from '../middleware/respond.js';
10
- import { ExportService } from '../services/import-export/index.js';
8
+ import { ExportService, ImportService } from '../services/import-export.js';
11
9
  import { RevisionsService } from '../services/revisions.js';
12
10
  import { UtilsService } from '../services/utils.js';
13
11
  import asyncHandler from '../utils/async-handler.js';
@@ -66,6 +64,10 @@ router.post('/import/:collection', collectionExists, asyncHandler(async (req, re
66
64
  if (req.is('multipart/form-data') === false) {
67
65
  throw new UnsupportedMediaTypeError({ mediaType: req.headers['content-type'], where: 'Content-Type header' });
68
66
  }
67
+ const service = new ImportService({
68
+ accountability: req.accountability,
69
+ schema: req.schema,
70
+ });
69
71
  let headers;
70
72
  if (req.headers['content-type']) {
71
73
  headers = req.headers;
@@ -78,34 +80,13 @@ router.post('/import/:collection', collectionExists, asyncHandler(async (req, re
78
80
  }
79
81
  const busboy = Busboy({ headers });
80
82
  busboy.on('file', async (_fieldname, fileStream, { mimeType }) => {
81
- const { createTmpFile } = await import('@directus/utils/node');
82
- const { getWorkerPool } = await import('../worker-pool.js');
83
- const tmpFile = await createTmpFile().catch(() => null);
84
- if (!tmpFile)
85
- throw new Error('Failed to create temporary file for import');
86
- fileStream.pipe(fs.createWriteStream(tmpFile.path));
87
- fileStream.on('end', async () => {
88
- const workerPool = getWorkerPool();
89
- const require = createRequire(import.meta.url);
90
- const filename = require.resolve('../services/import-export/import-worker');
91
- const workerData = {
92
- collection: req.params['collection'],
93
- mimeType,
94
- filePath: tmpFile.path,
95
- accountability: req.accountability,
96
- schema: req.schema,
97
- };
98
- try {
99
- await workerPool.run(workerData, { filename });
100
- res.status(200).end();
101
- }
102
- catch (error) {
103
- next(error);
104
- }
105
- finally {
106
- await tmpFile.cleanup();
107
- }
108
- });
83
+ try {
84
+ await service.import(req.params['collection'], mimeType, fileStream);
85
+ }
86
+ catch (err) {
87
+ return next(err);
88
+ }
89
+ return res.status(200).end();
109
90
  });
110
91
  busboy.on('error', (err) => next(err));
111
92
  req.pipe(busboy);
@@ -16,7 +16,7 @@ export declare function generateApiExtensionsSandboxEntrypoint(type: ApiExtensio
16
16
  unregisterFunction: () => Promise<void>;
17
17
  } | {
18
18
  code: string;
19
- hostFunctions: ((path: import("isolated-vm").Reference<string>, method: import("isolated-vm").Reference<"GET" | "POST" | "DELETE" | "PUT" | "PATCH">, cb: import("isolated-vm").Reference<(req: {
19
+ hostFunctions: ((path: import("isolated-vm").Reference<string>, method: import("isolated-vm").Reference<"GET" | "POST" | "DELETE" | "PATCH" | "PUT">, cb: import("isolated-vm").Reference<(req: {
20
20
  url: string;
21
21
  headers: import("http").IncomingHttpHeaders;
22
22
  body: string;
package/dist/flows.js CHANGED
@@ -19,6 +19,7 @@ import { mapValuesDeep } from './utils/map-values-deep.js';
19
19
  import { redactObject } from './utils/redact-object.js';
20
20
  import { sanitizeError } from './utils/sanitize-error.js';
21
21
  import { scheduleSynchronizedJob, validateCron } from './utils/schedule.js';
22
+ import { isSystemCollection } from '@directus/system-data';
22
23
  let flowManager;
23
24
  export function getFlowManager() {
24
25
  if (flowManager) {
@@ -111,7 +112,7 @@ class FlowManager {
111
112
  if (!flow.options?.['collections'])
112
113
  return [];
113
114
  return toArray(flow.options['collections']).map((collection) => {
114
- if (collection.startsWith('directus_')) {
115
+ if (isSystemCollection(collection)) {
115
116
  const action = scope.split('.')[1];
116
117
  return collection.substring(9) + '.' + action;
117
118
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Check if requested collection exists, and save it to req.collection
3
3
  */
4
- import { systemCollectionRows } from '../database/system-data/collections/index.js';
5
4
  import { ForbiddenError } from '@directus/errors';
5
+ import { systemCollectionRows } from '@directus/system-data';
6
6
  import asyncHandler from '../utils/async-handler.js';
7
7
  const collectionExists = asyncHandler(async (req, _res, next) => {
8
8
  if (!req.params['collection'])
@@ -11,11 +11,11 @@ const collectionExists = asyncHandler(async (req, _res, next) => {
11
11
  throw new ForbiddenError();
12
12
  }
13
13
  req.collection = req.params['collection'];
14
- if (req.collection.startsWith('directus_')) {
15
- const systemRow = systemCollectionRows.find((collection) => {
16
- return collection?.collection === req.collection;
17
- });
18
- req.singleton = !!systemRow?.singleton;
14
+ const systemCollectionRow = systemCollectionRows.find((collection) => {
15
+ return collection?.collection === req.collection;
16
+ });
17
+ if (systemCollectionRow !== undefined) {
18
+ req.singleton = !!systemCollectionRow?.singleton;
19
19
  }
20
20
  else {
21
21
  req.singleton = req.schema.collections[req.collection]?.singleton ?? false;
@@ -3,7 +3,7 @@ import { parse as parseBytesConfiguration } from 'bytes';
3
3
  import { assign } from 'lodash-es';
4
4
  import { getCache, setCacheValue } from '../cache.js';
5
5
  import { useLogger } from '../logger.js';
6
- import { ExportService } from '../services/import-export/index.js';
6
+ import { ExportService } from '../services/import-export.js';
7
7
  import { VersionsService } from '../services/versions.js';
8
8
  import asyncHandler from '../utils/async-handler.js';
9
9
  import { getCacheControlHeader } from '../utils/get-cache-headers.js';
@@ -24,12 +24,12 @@ export default defineOperationApi({
24
24
  return { status: result.status, statusText: result.statusText, headers: result.headers, data: result.data };
25
25
  }
26
26
  catch (error) {
27
- if (isAxiosError(error)) {
27
+ if (isAxiosError(error) && error.response) {
28
28
  throw JSON.stringify({
29
- status: error.response?.status,
30
- statusText: error.response?.statusText,
31
- headers: error.response?.headers,
32
- data: error.response?.data,
29
+ status: error.response.status,
30
+ statusText: error.response.statusText,
31
+ headers: error.response.headers,
32
+ data: error.response.data,
33
33
  });
34
34
  }
35
35
  else {
@@ -0,0 +1,11 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { Agent, ClientRequestArgs } from 'node:http';
3
+ /**
4
+ * 'createConnection' is missing in 'Agent' type, but assigned in actual implementation:
5
+ * https://github.com/nodejs/node/blob/8a41d9b636be86350cd32847c3f89d327c4f6ff7/lib/_http_agent.js#L215
6
+ */
7
+ export type _Agent = Agent & {
8
+ createConnection: NonNullable<ClientRequestArgs['createConnection']>;
9
+ };
10
+ /** Extends a HTTP agent with IP validation */
11
+ export declare const agentWithIpValidation: (agent: Agent) => Agent;
@@ -0,0 +1,34 @@
1
+ import { isIP } from 'node:net';
2
+ import { isDeniedIp } from './is-denied-ip.js';
3
+ const deniedError = (domain) => new Error(`Requested domain "${domain}" resolves to a denied IP address`);
4
+ /** Extends a HTTP agent with IP validation */
5
+ export const agentWithIpValidation = (agent) => {
6
+ const _agent = agent;
7
+ const { createConnection } = _agent;
8
+ _agent.createConnection = function (options, oncreate) {
9
+ const { host } = options;
10
+ /*
11
+ * Unexpected, but according to the types 'host' might be undefined.
12
+ * In that case, the request is denied to be on the safe side,
13
+ * since the host cannot be verified.
14
+ */
15
+ if (!host) {
16
+ throw new Error('Request cannot be verified due to missing host');
17
+ }
18
+ /*
19
+ * At this point, host is only verified if it's already an IP address.
20
+ * Otherwise it will be verified on 'lookup' event.
21
+ */
22
+ if (isIP(host) !== 0 && isDeniedIp(host))
23
+ throw deniedError(host);
24
+ const socket = createConnection.call(this, options, oncreate);
25
+ // Emitted after resolving the host name but before connecting.
26
+ socket.on('lookup', (error, address) => {
27
+ if (error || !isDeniedIp(address))
28
+ return;
29
+ return socket.destroy(deniedError(host));
30
+ });
31
+ return socket;
32
+ };
33
+ return agent;
34
+ };
@@ -1,14 +1,15 @@
1
- import { requestInterceptor } from './request-interceptor.js';
2
- import { responseInterceptor } from './response-interceptor.js';
3
1
  export const _cache = {
4
2
  axiosInstance: null,
5
3
  };
6
4
  export async function getAxios() {
7
5
  if (!_cache.axiosInstance) {
8
6
  const axios = (await import('axios')).default;
9
- _cache.axiosInstance = axios.create();
10
- _cache.axiosInstance.interceptors.request.use(requestInterceptor);
11
- _cache.axiosInstance.interceptors.response.use(responseInterceptor);
7
+ const { Agent: AgentHttp } = await import('node:http');
8
+ const { Agent: AgentHttps } = await import('node:https');
9
+ const { agentWithIpValidation } = await import('./agent-with-ip-validation.js');
10
+ const httpAgent = agentWithIpValidation(new AgentHttp());
11
+ const httpsAgent = agentWithIpValidation(new AgentHttps());
12
+ _cache.axiosInstance = axios.create({ httpAgent, httpsAgent });
12
13
  }
13
14
  return _cache.axiosInstance;
14
15
  }
@@ -0,0 +1 @@
1
+ export declare function isDeniedIp(ip: string): boolean;
@@ -2,34 +2,32 @@ import { useEnv } from '@directus/env';
2
2
  import os from 'node:os';
3
3
  import { useLogger } from '../logger.js';
4
4
  import { ipInNetworks } from '../utils/ip-in-networks.js';
5
- const deniedError = (url) => new Error(`Requested URL "${url}" resolves to a denied IP address`);
6
- export function validateIp(ip, url) {
5
+ export function isDeniedIp(ip) {
7
6
  const env = useEnv();
8
7
  const logger = useLogger();
9
8
  const ipDenyList = env['IMPORT_IP_DENY_LIST'];
10
9
  if (ipDenyList.length === 0)
11
- return;
12
- let denied;
10
+ return false;
13
11
  try {
14
- denied = ipInNetworks(ip, ipDenyList);
12
+ const denied = ipInNetworks(ip, ipDenyList);
13
+ if (denied)
14
+ return true;
15
15
  }
16
16
  catch (error) {
17
- logger.warn(`Invalid "IMPORT_IP_DENY_LIST" configuration`);
17
+ logger.warn(`Cannot verify IP address due to invalid "IMPORT_IP_DENY_LIST" config`);
18
18
  logger.warn(error);
19
- throw deniedError(url);
19
+ return true;
20
20
  }
21
- if (denied)
22
- throw deniedError(url);
23
21
  if (ipDenyList.includes('0.0.0.0')) {
24
22
  const networkInterfaces = os.networkInterfaces();
25
23
  for (const networkInfo of Object.values(networkInterfaces)) {
26
24
  if (!networkInfo)
27
25
  continue;
28
26
  for (const info of networkInfo) {
29
- if (info.address === ip) {
30
- throw deniedError(url);
31
- }
27
+ if (info.address === ip)
28
+ return true;
32
29
  }
33
30
  }
34
31
  }
32
+ return false;
35
33
  }
@@ -24,7 +24,7 @@ export class AssetsService {
24
24
  constructor(options) {
25
25
  this.knex = options.knex || getDatabase();
26
26
  this.accountability = options.accountability || null;
27
- this.filesService = new FilesService(options);
27
+ this.filesService = new FilesService({ ...options, accountability: null });
28
28
  this.authorizationService = new AuthorizationService(options);
29
29
  }
30
30
  async getAsset(id, transformation, range) {
@@ -46,8 +46,6 @@ export class AssetsService {
46
46
  await this.authorizationService.checkAccess('read', 'directus_files', id);
47
47
  }
48
48
  const file = (await this.filesService.readOne(id, { limit: 1 }));
49
- if (!file)
50
- throw new ForbiddenError();
51
49
  const exists = await storage.location(file.storage).exists(file.filename_disk);
52
50
  if (!exists)
53
51
  throw new ForbiddenError();
@@ -13,5 +13,5 @@ export declare class AuthorizationService {
13
13
  * Checks if the provided payload matches the configured permissions, and adds the presets to the payload.
14
14
  */
15
15
  validatePayload(action: PermissionsAction, collection: string, data: Partial<Item>): Partial<Item>;
16
- checkAccess(action: PermissionsAction, collection: string, pk: PrimaryKey | PrimaryKey[]): Promise<void>;
16
+ checkAccess(action: PermissionsAction, collection: string, pk?: PrimaryKey | PrimaryKey[]): Promise<void>;
17
17
  }
@@ -1,9 +1,9 @@
1
+ import { ForbiddenError } from '@directus/errors';
1
2
  import { validatePayload } from '@directus/utils';
2
3
  import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
4
  import { cloneDeep, flatten, isArray, isNil, merge, reduce, uniq, uniqWith } from 'lodash-es';
4
5
  import { GENERATE_SPECIAL } from '../constants.js';
5
6
  import getDatabase from '../database/index.js';
6
- import { ForbiddenError } from '@directus/errors';
7
7
  import { getRelationInfo } from '../utils/get-relation-info.js';
8
8
  import { stripFunction } from '../utils/strip-function.js';
9
9
  import { ItemsService } from './items.js';
@@ -430,15 +430,27 @@ export class AuthorizationService {
430
430
  };
431
431
  if (Array.isArray(pk)) {
432
432
  const result = await itemsService.readMany(pk, { ...query, limit: pk.length }, { permissionsAction: action });
433
- if (!result)
433
+ // for the unexpected case that the result is not an array (for example due to filter hook)
434
+ if (!isArray(result))
434
435
  throw new ForbiddenError();
435
436
  if (result.length !== pk.length)
436
437
  throw new ForbiddenError();
437
438
  }
438
- else {
439
+ else if (pk) {
439
440
  const result = await itemsService.readOne(pk, query, { permissionsAction: action });
440
441
  if (!result)
441
442
  throw new ForbiddenError();
442
443
  }
444
+ else {
445
+ query.limit = 1;
446
+ const result = await itemsService.readByQuery(query, { permissionsAction: action });
447
+ // for the unexpected case that the result is not an array (for example due to filter hook)
448
+ if (!isArray(result))
449
+ throw new ForbiddenError();
450
+ // for create action, an empty array is expected - for other actions, the first item is expected to be available
451
+ const access = action === 'create' ? result.length === 0 : !!result[0];
452
+ if (!access)
453
+ throw new ForbiddenError();
454
+ }
443
455
  }
444
456
  }
@@ -3,12 +3,13 @@ import type { Accountability, RawField, SchemaOverview } from '@directus/types';
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
6
- import type { AbstractServiceOptions, Collection, CollectionMeta, MutationOptions } from '../types/index.js';
6
+ import type { AbstractServiceOptions, Collection, MutationOptions } from '../types/index.js';
7
+ import { type BaseCollectionMeta } from '@directus/system-data';
7
8
  export type RawCollection = {
8
9
  collection: string;
9
10
  fields?: RawField[];
10
11
  schema?: Partial<Table> | null;
11
- meta?: Partial<CollectionMeta> | null;
12
+ meta?: Partial<BaseCollectionMeta> | null;
12
13
  };
13
14
  export declare class CollectionsService {
14
15
  knex: Knex;
@@ -7,12 +7,12 @@ import { clearSystemCache, getCache } from '../cache.js';
7
7
  import { ALIAS_TYPES } from '../constants.js';
8
8
  import { getHelpers } from '../database/helpers/index.js';
9
9
  import getDatabase, { getSchemaInspector } from '../database/index.js';
10
- import { systemCollectionRows } from '../database/system-data/collections/index.js';
11
10
  import emitter from '../emitter.js';
12
11
  import { getSchema } from '../utils/get-schema.js';
13
12
  import { shouldClearCache } from '../utils/should-clear-cache.js';
14
13
  import { FieldsService } from './fields.js';
15
14
  import { ItemsService } from './items.js';
15
+ import { systemCollectionRows } from '@directus/system-data';
16
16
  export class CollectionsService {
17
17
  knex;
18
18
  helpers;
@@ -7,7 +7,6 @@ import { ALIAS_TYPES } from '../constants.js';
7
7
  import { translateDatabaseError } from '../database/errors/translate.js';
8
8
  import { getHelpers } from '../database/helpers/index.js';
9
9
  import getDatabase, { getSchemaInspector } from '../database/index.js';
10
- import { systemFieldRows } from '../database/system-data/fields/index.js';
11
10
  import emitter from '../emitter.js';
12
11
  import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
13
12
  import { ItemsService } from './items.js';
@@ -18,6 +17,8 @@ import { getSchema } from '../utils/get-schema.js';
18
17
  import { sanitizeColumn } from '../utils/sanitize-schema.js';
19
18
  import { shouldClearCache } from '../utils/should-clear-cache.js';
20
19
  import { RelationsService } from './relations.js';
20
+ import { getSystemFieldRowsWithAuthProviders } from '../utils/get-field-system-rows.js';
21
+ const systemFieldRows = getSystemFieldRowsWithAuthProviders();
21
22
  export class FieldsService {
22
23
  knex;
23
24
  helpers;
@@ -288,11 +288,12 @@ export class FilesService extends ItemsService {
288
288
  decompress: false,
289
289
  });
290
290
  }
291
- catch (err) {
292
- logger.warn(err, `Couldn't fetch file from URL "${importURL}"`);
291
+ catch (error) {
292
+ logger.warn(`Couldn't fetch file from URL "${importURL}"${error.message ? `: ${error.message}` : ''}`);
293
+ logger.trace(error);
293
294
  throw new ServiceUnavailableError({
294
295
  service: 'external-file',
295
- reason: `Couldn't fetch file from url "${importURL}"`,
296
+ reason: `Couldn't fetch file from URL "${importURL}"`,
296
297
  });
297
298
  }
298
299
  const parsedURL = url.parse(fileResponse.request.res.responseUrl);
@@ -40,6 +40,7 @@ import { GraphQLStringOrFloat } from './types/string-or-float.js';
40
40
  import { GraphQLVoid } from './types/void.js';
41
41
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
42
42
  import processError from './utils/process-error.js';
43
+ import { isSystemCollection } from '@directus/system-data';
43
44
  const env = useEnv();
44
45
  const validationRules = Array.from(specifiedRules);
45
46
  if (env['GRAPHQL_INTROSPECTION'] === false) {
@@ -129,10 +130,10 @@ export class GraphQLService {
129
130
  const { ReadCollectionTypes, VersionCollectionTypes } = getReadableTypes();
130
131
  const { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes } = getWritableTypes();
131
132
  const scopeFilter = (collection) => {
132
- if (this.scope === 'items' && collection.collection.startsWith('directus_') === true)
133
+ if (this.scope === 'items' && isSystemCollection(collection.collection))
133
134
  return false;
134
135
  if (this.scope === 'system') {
135
- if (collection.collection.startsWith('directus_') === false)
136
+ if (isSystemCollection(collection.collection) === false)
136
137
  return false;
137
138
  if (SYSTEM_DENY_LIST.includes(collection.collection))
138
139
  return false;
@@ -2,7 +2,7 @@
2
2
  import type { Accountability, File, Query, SchemaOverview } from '@directus/types';
3
3
  import type { Knex } from 'knex';
4
4
  import type { Readable } from 'node:stream';
5
- import type { AbstractServiceOptions } from '../../types/index.js';
5
+ import type { AbstractServiceOptions } from '../types/index.js';
6
6
  type ExportFormat = 'csv' | 'json' | 'xml' | 'yaml';
7
7
  export declare class ImportService {
8
8
  knex: Knex;
@@ -10,16 +10,17 @@ import { createReadStream } from 'node:fs';
10
10
  import { appendFile } from 'node:fs/promises';
11
11
  import Papa from 'papaparse';
12
12
  import StreamArray from 'stream-json/streamers/StreamArray.js';
13
- import getDatabase from '../../database/index.js';
14
- import emitter from '../../emitter.js';
15
- import { useLogger } from '../../logger.js';
16
- import { getDateFormatted } from '../../utils/get-date-formatted.js';
17
- import { Url } from '../../utils/url.js';
18
- import { userName } from '../../utils/user-name.js';
19
- import { FilesService } from '../files.js';
20
- import { ItemsService } from '../items.js';
21
- import { NotificationsService } from '../notifications.js';
22
- import { UsersService } from '../users.js';
13
+ import getDatabase from '../database/index.js';
14
+ import emitter from '../emitter.js';
15
+ import { useLogger } from '../logger.js';
16
+ import { getDateFormatted } from '../utils/get-date-formatted.js';
17
+ import { Url } from '../utils/url.js';
18
+ import { userName } from '../utils/user-name.js';
19
+ import { FilesService } from './files.js';
20
+ import { ItemsService } from './items.js';
21
+ import { NotificationsService } from './notifications.js';
22
+ import { UsersService } from './users.js';
23
+ import { isSystemCollection } from '@directus/system-data';
23
24
  const env = useEnv();
24
25
  const logger = useLogger();
25
26
  export class ImportService {
@@ -32,7 +33,7 @@ export class ImportService {
32
33
  this.schema = options.schema;
33
34
  }
34
35
  async import(collection, mimetype, stream) {
35
- if (this.accountability?.admin !== true && collection.startsWith('directus_'))
36
+ if (this.accountability?.admin !== true && isSystemCollection(collection))
36
37
  throw new ForbiddenError();
37
38
  const createPermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'create');
38
39
  const updatePermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'update');
@@ -10,7 +10,7 @@ export * from './files.js';
10
10
  export * from './flows.js';
11
11
  export * from './folders.js';
12
12
  export * from './graphql/index.js';
13
- export * from './import-export/index.js';
13
+ export * from './import-export.js';
14
14
  export * from './items.js';
15
15
  export * from './mail/index.js';
16
16
  export * from './meta.js';
@@ -10,7 +10,7 @@ export * from './files.js';
10
10
  export * from './flows.js';
11
11
  export * from './folders.js';
12
12
  export * from './graphql/index.js';
13
- export * from './import-export/index.js';
13
+ export * from './import-export.js';
14
14
  export * from './items.js';
15
15
  export * from './mail/index.js';
16
16
  export * from './meta.js';
@@ -13,6 +13,7 @@ import { shouldClearCache } from '../utils/should-clear-cache.js';
13
13
  import { validateKeys } from '../utils/validate-keys.js';
14
14
  import { AuthorizationService } from './authorization.js';
15
15
  import { PayloadService } from './payload.js';
16
+ import { isSystemCollection } from '@directus/system-data';
16
17
  const env = useEnv();
17
18
  export class ItemsService {
18
19
  collection;
@@ -25,7 +26,7 @@ export class ItemsService {
25
26
  this.collection = collection;
26
27
  this.knex = options.knex || getDatabase();
27
28
  this.accountability = options.accountability || null;
28
- this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
29
+ this.eventScope = isSystemCollection(this.collection) ? this.collection.substring(9) : 'items';
29
30
  this.schema = options.schema;
30
31
  this.cache = getCache().cache;
31
32
  return this;
@@ -1,8 +1,8 @@
1
- import type { PermissionsAction, Query } from '@directus/types';
1
+ import type { ItemPermissions, PermissionsAction, Query } from '@directus/types';
2
2
  import type Keyv from 'keyv';
3
+ import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
3
4
  import type { QueryOptions } from './items.js';
4
5
  import { ItemsService } from './items.js';
5
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
6
6
  export declare class PermissionsService extends ItemsService {
7
7
  systemCache: Keyv<any>;
8
8
  constructor(options: AbstractServiceOptions);
@@ -15,4 +15,5 @@ export declare class PermissionsService extends ItemsService {
15
15
  updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
16
16
  upsertMany(payloads: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
17
17
  deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
18
+ getItemPermissions(collection: string, primaryKey?: string): Promise<ItemPermissions>;
18
19
  }