@directus/api 22.2.0 → 23.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/auth/drivers/ldap.js +14 -3
  3. package/dist/auth/drivers/oauth2.js +13 -2
  4. package/dist/auth/drivers/openid.js +13 -2
  5. package/dist/cache.js +4 -4
  6. package/dist/cli/commands/init/questions.d.ts +5 -5
  7. package/dist/cli/commands/schema/apply.d.ts +1 -0
  8. package/dist/cli/commands/schema/apply.js +20 -1
  9. package/dist/cli/index.js +1 -0
  10. package/dist/cli/utils/create-env/env-stub.liquid +1 -4
  11. package/dist/controllers/activity.js +30 -27
  12. package/dist/controllers/assets.js +1 -1
  13. package/dist/controllers/comments.d.ts +2 -0
  14. package/dist/controllers/comments.js +153 -0
  15. package/dist/controllers/versions.js +10 -5
  16. package/dist/database/index.js +3 -0
  17. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  18. package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
  19. package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
  20. package/dist/database/migrations/20240909A-separate-comments.js +65 -0
  21. package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
  22. package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
  23. package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
  24. package/dist/database/run-ast/lib/get-db-query.js +2 -2
  25. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
  26. package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
  27. package/dist/database/run-ast/run-ast.js +8 -1
  28. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
  29. package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
  30. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
  31. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
  32. package/dist/logger/index.d.ts +2 -3
  33. package/dist/logger/logs-stream.d.ts +0 -1
  34. package/dist/mailer.js +0 -6
  35. package/dist/middleware/authenticate.d.ts +1 -3
  36. package/dist/middleware/error-handler.d.ts +0 -1
  37. package/dist/middleware/validate-batch.d.ts +1 -4
  38. package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
  39. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
  40. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
  41. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
  42. package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
  43. package/dist/permissions/modules/validate-access/validate-access.js +14 -1
  44. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
  46. package/dist/permissions/utils/process-permissions.d.ts +11 -1
  47. package/dist/permissions/utils/process-permissions.js +6 -4
  48. package/dist/request/agent-with-ip-validation.d.ts +0 -1
  49. package/dist/server.d.ts +0 -3
  50. package/dist/services/activity.d.ts +1 -7
  51. package/dist/services/activity.js +0 -103
  52. package/dist/services/assets.d.ts +0 -1
  53. package/dist/services/assets.js +5 -4
  54. package/dist/services/collections.js +6 -4
  55. package/dist/services/comments.d.ts +31 -0
  56. package/dist/services/comments.js +374 -0
  57. package/dist/services/fields.js +0 -6
  58. package/dist/services/files/utils/get-metadata.d.ts +0 -1
  59. package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
  60. package/dist/services/files.d.ts +0 -1
  61. package/dist/services/graphql/index.js +17 -16
  62. package/dist/services/import-export.d.ts +0 -1
  63. package/dist/services/index.d.ts +1 -0
  64. package/dist/services/index.js +1 -0
  65. package/dist/services/items.js +3 -1
  66. package/dist/services/mail/index.d.ts +2 -1
  67. package/dist/services/mail/index.js +4 -1
  68. package/dist/services/payload.js +15 -14
  69. package/dist/services/tus/data-store.d.ts +0 -1
  70. package/dist/services/users.js +3 -2
  71. package/dist/services/versions.js +59 -44
  72. package/dist/types/graphql.d.ts +0 -1
  73. package/dist/utils/apply-diff.js +5 -6
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/compress.d.ts +0 -1
  76. package/dist/utils/delete-from-require-cache.js +1 -1
  77. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
  78. package/dist/utils/generate-hash.js +2 -2
  79. package/dist/utils/get-address.d.ts +0 -3
  80. package/dist/utils/get-cache-headers.d.ts +0 -1
  81. package/dist/utils/get-cache-key.d.ts +0 -1
  82. package/dist/utils/get-column.d.ts +1 -1
  83. package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
  84. package/dist/utils/get-ip-from-req.d.ts +0 -1
  85. package/dist/utils/get-service.js +3 -1
  86. package/dist/utils/get-snapshot.js +1 -1
  87. package/dist/utils/sanitize-query.js +1 -1
  88. package/dist/utils/sanitize-schema.d.ts +1 -1
  89. package/dist/utils/sanitize-schema.js +2 -0
  90. package/dist/utils/should-skip-cache.d.ts +0 -1
  91. package/dist/websocket/authenticate.js +1 -1
  92. package/dist/websocket/controllers/base.d.ts +1 -10
  93. package/dist/websocket/controllers/base.js +15 -21
  94. package/dist/websocket/controllers/graphql.d.ts +0 -3
  95. package/dist/websocket/controllers/index.d.ts +0 -3
  96. package/dist/websocket/controllers/logs.d.ts +4 -5
  97. package/dist/websocket/controllers/logs.js +7 -3
  98. package/dist/websocket/controllers/rest.d.ts +0 -3
  99. package/dist/websocket/controllers/rest.js +1 -1
  100. package/dist/websocket/types.d.ts +0 -6
  101. package/package.json +70 -71
@@ -0,0 +1,10 @@
1
+ export async function up(knex) {
2
+ await knex.schema.alterTable('directus_versions', (table) => {
3
+ table.json('delta');
4
+ });
5
+ }
6
+ export async function down(knex) {
7
+ await knex.schema.alterTable('directus_versions', (table) => {
8
+ table.dropColumn('delta');
9
+ });
10
+ }
@@ -1,4 +1,14 @@
1
- import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
+ import type { Context } from '../../../permissions/types.js';
3
4
  import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
4
- export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): Knex.QueryBuilder;
5
+ export type DBQueryOptions = {
6
+ table: string;
7
+ fieldNodes: (FieldNode | FunctionFieldNode)[];
8
+ o2mNodes: O2MNode[];
9
+ query: Query;
10
+ cases: Filter[];
11
+ permissions: Permission[];
12
+ permissionsOnly?: boolean;
13
+ };
14
+ export declare function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }: DBQueryOptions, { knex, schema }: Context): Knex.QueryBuilder;
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
9
9
  import { getNodeAlias } from '../utils/get-field-alias.js';
10
10
  import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
11
11
  import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
12
- export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
12
+ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }, { knex, schema }) {
13
13
  const aliasMap = Object.create(null);
14
14
  const env = useEnv();
15
- const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
15
+ const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly);
16
16
  const queryCopy = cloneDeep(query);
17
17
  const helpers = getHelpers(knex);
18
18
  const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
@@ -0,0 +1,15 @@
1
+ import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { AST } from '../../../types/ast.js';
4
+ type FetchPermittedAstRootFieldsOptions = {
5
+ schema: SchemaOverview;
6
+ accountability: Accountability;
7
+ knex: Knex;
8
+ action: PermissionsAction;
9
+ };
10
+ /**
11
+ * Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
12
+ * same way as `runAst` but only returns flags (1/null) instead of actual field values.
13
+ */
14
+ export declare function fetchPermittedAstRootFields(originalAST: AST, { schema, accountability, knex, action }: FetchPermittedAstRootFieldsOptions): Promise<any>;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ import { cloneDeep } from 'lodash-es';
2
+ import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
4
+ import { getDBQuery } from '../lib/get-db-query.js';
5
+ import { parseCurrentLevel } from '../lib/parse-current-level.js';
6
+ /**
7
+ * Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
8
+ * same way as `runAst` but only returns flags (1/null) instead of actual field values.
9
+ */
10
+ export async function fetchPermittedAstRootFields(originalAST, { schema, accountability, knex, action }) {
11
+ const ast = cloneDeep(originalAST);
12
+ const { name: collection, children, cases, query } = ast;
13
+ // Retrieve the database columns to select in the current AST
14
+ const { fieldNodes } = await parseCurrentLevel(schema, collection, children, query);
15
+ let permissions = [];
16
+ if (accountability && !accountability.admin) {
17
+ const policies = await fetchPolicies(accountability, { schema, knex });
18
+ permissions = await fetchPermissions({ action, accountability, policies }, { schema, knex });
19
+ }
20
+ return getDBQuery({
21
+ table: collection,
22
+ fieldNodes,
23
+ o2mNodes: [],
24
+ query,
25
+ cases,
26
+ permissions,
27
+ permissionsOnly: true,
28
+ }, { schema, knex });
29
+ }
@@ -36,7 +36,14 @@ export async function runAst(originalAST, schema, accountability, options) {
36
36
  permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
37
37
  }
38
38
  // The actual knex query builder instance. This is a promise that resolves with the raw items from the db
39
- const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
39
+ const dbQuery = getDBQuery({
40
+ table: collection,
41
+ fieldNodes,
42
+ o2mNodes,
43
+ query,
44
+ cases,
45
+ permissions,
46
+ }, { schema, knex });
40
47
  const rawItems = await dbQuery;
41
48
  if (!rawItems)
42
49
  return null;
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
6
6
  /** Don't assign an alias to the column but instead return the column as is */
7
7
  noAlias?: boolean;
8
8
  }
9
- export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
9
+ export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, permissionsOnly?: boolean): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
10
10
  export {};
@@ -4,7 +4,7 @@ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
4
4
  import { getHelpers } from '../../helpers/index.js';
5
5
  import { applyCaseWhen } from './apply-case-when.js';
6
6
  import { getNodeAlias } from './get-field-alias.js';
7
- export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
7
+ export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly) {
8
8
  const helpers = getHelpers(knex);
9
9
  return function (fieldNode, options) {
10
10
  // Don't assign an alias to the column expression if the field has a whenCase
@@ -22,7 +22,15 @@ export function getColumnPreprocessor(knex, schema, table, cases, permissions, a
22
22
  field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
23
23
  }
24
24
  let column;
25
- if (field?.type?.startsWith('geometry')) {
25
+ if (permissionsOnly) {
26
+ if (noAlias) {
27
+ column = knex.raw(1);
28
+ }
29
+ else {
30
+ column = knex.raw('1 as ??', [alias]);
31
+ }
32
+ }
33
+ else if (field?.type?.startsWith('geometry')) {
26
34
  column = helpers.st.asText(table, field.field, rawColumnAlias);
27
35
  }
28
36
  else if (fieldNode.type === 'functionField') {
@@ -1,6 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node/http.js" />
3
- /// <reference types="pino-http" />
4
1
  import type { ApiExtensionType, HybridExtensionType } from '@directus/extensions';
5
2
  import type { Router } from 'express';
6
3
  /**
@@ -1,9 +1,8 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Router } from 'express';
3
2
  import type { Reference } from 'isolated-vm';
4
3
  import type { IncomingHttpHeaders } from 'node:http';
5
4
  export declare function registerRouteGenerator(endpointName: string, endpointRouter: Router): {
6
- register: (path: Reference<string>, method: Reference<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>, cb: Reference<(req: {
5
+ register: (path: Reference<string>, method: Reference<"GET" | "POST" | "PUT" | "PATCH" | "DELETE">, cb: Reference<(req: {
7
6
  url: string;
8
7
  headers: IncomingHttpHeaders;
9
8
  body: string;
@@ -1,4 +1,3 @@
1
- /// <reference types="qs" />
2
1
  import type { RequestHandler } from 'express';
3
2
  import { type Logger } from 'pino';
4
3
  import { LogsStream } from './logs-stream.js';
@@ -11,5 +10,5 @@ export declare const useLogger: () => Logger<never>;
11
10
  export declare const getLogsStream: (pretty: boolean) => LogsStream;
12
11
  export declare const getHttpLogsStream: (pretty: boolean) => LogsStream;
13
12
  export declare const getLoggerLevelValue: (level: string) => number;
14
- export declare const createLogger: () => Logger<never>;
15
- export declare const createExpressLogger: () => RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
13
+ export declare const createLogger: () => Logger<never, boolean>;
14
+ export declare const createExpressLogger: () => RequestHandler;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Bus } from '@directus/memory';
3
2
  import { Writable } from 'stream';
4
3
  type PrettyType = 'basic' | 'http' | false;
package/dist/mailer.js CHANGED
@@ -56,12 +56,6 @@ export default function getMailer() {
56
56
  host: env['EMAIL_MAILGUN_HOST'] || 'api.mailgun.net',
57
57
  }));
58
58
  }
59
- else if (transportName === 'sendgrid') {
60
- const sg = require('nodemailer-sendgrid');
61
- transporter = nodemailer.createTransport(sg({
62
- apiKey: env['EMAIL_SENDGRID_API_KEY'],
63
- }));
64
- }
65
59
  else {
66
60
  logger.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
67
61
  }
@@ -1,9 +1,7 @@
1
- /// <reference types="qs" />
2
- /// <reference types="cookie-parser" />
3
1
  import type { NextFunction, Request, Response } from 'express';
4
2
  /**
5
3
  * Verify the passed JWT and assign the user ID and role to `req`
6
4
  */
7
5
  export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
8
- declare const _default: (req: Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: NextFunction) => Promise<void>;
6
+ declare const _default: (req: Request, res: Response, next: NextFunction) => Promise<void>;
9
7
  export default _default;
@@ -1,3 +1,2 @@
1
- /// <reference types="qs" />
2
1
  import type { ErrorRequestHandler } from 'express';
3
2
  export declare const errorHandler: (err: any, req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction) => Promise<ReturnType<ErrorRequestHandler>>;
@@ -1,4 +1 @@
1
- /// <reference types="qs" />
2
- /// <reference types="express" />
3
- /// <reference types="cookie-parser" />
4
- export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
1
+ export declare const validateBatch: (scope: "read" | "update" | "delete") => (req: import("express").Request, res: import("express").Response, next: import("express").NextFunction) => Promise<void>;
@@ -7,4 +7,14 @@ export interface FetchPermissionsOptions {
7
7
  accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
8
8
  bypassDynamicVariableProcessing?: boolean;
9
9
  }
10
- export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<import("@directus/types").Permission[]>;
10
+ export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<{
11
+ permissions: import("@directus/types").Filter | null;
12
+ validation: import("@directus/types").Filter | null;
13
+ presets: any;
14
+ id?: number;
15
+ policy: string | null;
16
+ collection: string;
17
+ action: PermissionsAction;
18
+ fields: string[] | null;
19
+ system?: true;
20
+ }[]>;
@@ -1,5 +1,5 @@
1
1
  import type { CollectionKey, FieldMap, QueryPath } from '../types.js';
2
2
  export declare function getInfoForPath(fieldMap: FieldMap, group: keyof FieldMap, path: QueryPath, collection: CollectionKey): {
3
- collection: string;
4
- fields: Set<string>;
3
+ collection: CollectionKey;
4
+ fields: Set<import("../types.js").FieldKey>;
5
5
  };
@@ -5,5 +5,6 @@ export interface ValidateItemAccessOptions {
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
7
  primaryKeys: PrimaryKey[];
8
+ fields?: string[];
8
9
  }
9
- export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<boolean>;
10
+ export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<any>;
@@ -1,5 +1,4 @@
1
- import { getAstFromQuery } from '../../../../database/get-ast-from-query/get-ast-from-query.js';
2
- import { runAst } from '../../../../database/run-ast/run-ast.js';
1
+ import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
3
2
  import { processAst } from '../../process-ast/process-ast.js';
4
3
  export async function validateItemAccess(options, context) {
5
4
  const primaryKeyField = context.schema.collections[options.collection]?.primary;
@@ -8,17 +7,14 @@ export async function validateItemAccess(options, context) {
8
7
  }
9
8
  // When we're looking up access to specific items, we have to read them from the database to
10
9
  // make sure you are allowed to access them.
11
- const query = {
12
- // We don't actually need any of the field data, just want to know if we can read the item as
13
- // whole or not
14
- fields: [],
15
- limit: options.primaryKeys.length,
10
+ const ast = {
11
+ type: 'root',
12
+ name: options.collection,
13
+ query: { limit: options.primaryKeys.length },
14
+ // Act as if every field was a "normal" field
15
+ children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
16
+ cases: [],
16
17
  };
17
- const ast = await getAstFromQuery({
18
- accountability: options.accountability,
19
- query,
20
- collection: options.collection,
21
- }, context);
22
18
  await processAst({ ast, ...options }, context);
23
19
  // Inject the filter after the permissions have been processed, as to not require access to the primary key
24
20
  ast.query.filter = {
@@ -26,8 +22,17 @@ export async function validateItemAccess(options, context) {
26
22
  _in: options.primaryKeys,
27
23
  },
28
24
  };
29
- const items = await runAst(ast, context.schema, options.accountability, { knex: context.knex });
25
+ const items = await fetchPermittedAstRootFields(ast, {
26
+ schema: context.schema,
27
+ accountability: options.accountability,
28
+ knex: context.knex,
29
+ action: options.action,
30
+ });
30
31
  if (items && items.length === options.primaryKeys.length) {
32
+ const { fields } = options;
33
+ if (fields) {
34
+ return items.every((item) => fields.every((field) => item[field] === 1));
35
+ }
31
36
  return true;
32
37
  }
33
38
  return false;
@@ -5,6 +5,7 @@ export interface ValidateAccessOptions {
5
5
  action: PermissionsAction;
6
6
  collection: string;
7
7
  primaryKeys?: PrimaryKey[];
8
+ fields?: string[];
8
9
  }
9
10
  /**
10
11
  * Validate if the current user has access to perform action against the given collection and
@@ -7,6 +7,12 @@ import { validateItemAccess } from './lib/validate-item-access.js';
7
7
  * control rules and checking if we got the expected result back
8
8
  */
9
9
  export async function validateAccess(options, context) {
10
+ // Skip further validation if the collection does not exist
11
+ if (options.collection in context.schema.collections === false) {
12
+ throw new ForbiddenError({
13
+ reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
14
+ });
15
+ }
10
16
  if (options.accountability.admin === true) {
11
17
  return;
12
18
  }
@@ -21,8 +27,15 @@ export async function validateAccess(options, context) {
21
27
  access = await validateCollectionAccess(options, context);
22
28
  }
23
29
  if (!access) {
30
+ if (options.fields?.length ?? 0 > 0) {
31
+ throw new ForbiddenError({
32
+ reason: `You don't have permissions to perform "${options.action}" for the field(s) ${options
33
+ .fields.map((field) => `"${field}"`)
34
+ .join(', ')} in collection "${options.collection}" or it does not exist.`,
35
+ });
36
+ }
24
37
  throw new ForbiddenError({
25
- reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
38
+ reason: `You don't have permission to perform "${options.action}" for collection "${options.collection}" or it does not exist.`,
26
39
  });
27
40
  }
28
41
  }
@@ -1,5 +1,4 @@
1
1
  import { type FetchUserCountOptions } from '../../../utils/fetch-user-count/fetch-user-count.js';
2
2
  import type { Context } from '../../types.js';
3
- export interface ValidateRemainingAdminUsersOptions extends Pick<FetchUserCountOptions, 'excludeAccessRows' | 'excludePolicies' | 'excludeUsers' | 'excludeRoles'> {
4
- }
3
+ export type ValidateRemainingAdminUsersOptions = Pick<FetchUserCountOptions, 'excludeAccessRows' | 'excludePolicies' | 'excludeUsers' | 'excludeRoles'>;
5
4
  export declare function validateRemainingAdminUsers(options: ValidateRemainingAdminUsersOptions, context: Context): Promise<void>;
@@ -32,13 +32,21 @@ export async function fetchDynamicVariableContext(options, context) {
32
32
  });
33
33
  });
34
34
  }
35
- if (options.policies.length > 0 && (permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
36
- contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
37
- const policiesService = new PoliciesService(context);
38
- return await policiesService.readMany(options.policies, {
39
- fields,
35
+ if (options.policies.length > 0) {
36
+ if ((permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
37
+ // Always add the id field
38
+ permissionContext.$CURRENT_POLICIES.add('id');
39
+ contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
40
+ const policiesService = new PoliciesService(context);
41
+ return await policiesService.readMany(options.policies, {
42
+ fields,
43
+ });
40
44
  });
41
- });
45
+ }
46
+ else {
47
+ // Always create entries for the policies with the `id` field present
48
+ contextData['$CURRENT_POLICIES'] = options.policies.map((id) => ({ id }));
49
+ }
42
50
  }
43
51
  return contextData;
44
52
  }
@@ -4,4 +4,14 @@ export interface ProcessPermissionsOptions {
4
4
  accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
5
5
  permissionsContext: Record<string, any>;
6
6
  }
7
- export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions): Permission[];
7
+ export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions): {
8
+ permissions: import("@directus/types").Filter | null;
9
+ validation: import("@directus/types").Filter | null;
10
+ presets: any;
11
+ id?: number;
12
+ policy: string | null;
13
+ collection: string;
14
+ action: import("@directus/types").PermissionsAction;
15
+ fields: string[] | null;
16
+ system?: true;
17
+ }[];
@@ -1,9 +1,11 @@
1
1
  import { parseFilter, parsePreset } from '@directus/utils';
2
2
  export function processPermissions({ permissions, accountability, permissionsContext }) {
3
3
  return permissions.map((permission) => {
4
- permission.permissions = parseFilter(permission.permissions, accountability, permissionsContext);
5
- permission.validation = parseFilter(permission.validation, accountability, permissionsContext);
6
- permission.presets = parsePreset(permission.presets, accountability, permissionsContext);
7
- return permission;
4
+ return {
5
+ ...permission,
6
+ permissions: parseFilter(permission.permissions, accountability, permissionsContext),
7
+ validation: parseFilter(permission.validation, accountability, permissionsContext),
8
+ presets: parsePreset(permission.presets, accountability, permissionsContext),
9
+ };
8
10
  });
9
11
  }
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Agent, ClientRequestArgs } from 'node:http';
3
2
  /**
4
3
  * 'createConnection' is missing in 'Agent' type, but assigned in actual implementation:
package/dist/server.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node/http.js" />
3
- /// <reference types="pino-http" />
4
1
  import * as http from 'http';
5
2
  export declare let SERVER_ONLINE: boolean;
6
3
  export declare function createServer(): Promise<http.Server>;
@@ -1,11 +1,5 @@
1
- import type { Item, PrimaryKey } from '@directus/types';
2
- import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
1
+ import type { AbstractServiceOptions } from '../types/index.js';
3
2
  import { ItemsService } from './items.js';
4
- import { NotificationsService } from './notifications.js';
5
- import { UsersService } from './users.js';
6
3
  export declare class ActivityService extends ItemsService {
7
- notificationsService: NotificationsService;
8
- usersService: UsersService;
9
4
  constructor(options: AbstractServiceOptions);
10
- createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
11
5
  }
@@ -1,109 +1,6 @@
1
- import { Action } from '@directus/constants';
2
- import { useEnv } from '@directus/env';
3
- import { ErrorCode, isDirectusError } from '@directus/errors';
4
- import { uniq } from 'lodash-es';
5
- import { useLogger } from '../logger/index.js';
6
- import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
7
- import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
8
- import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
9
- import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
10
- import { isValidUuid } from '../utils/is-valid-uuid.js';
11
- import { Url } from '../utils/url.js';
12
- import { userName } from '../utils/user-name.js';
13
1
  import { ItemsService } from './items.js';
14
- import { NotificationsService } from './notifications.js';
15
- import { UsersService } from './users.js';
16
- const env = useEnv();
17
- const logger = useLogger();
18
2
  export class ActivityService extends ItemsService {
19
- notificationsService;
20
- usersService;
21
3
  constructor(options) {
22
4
  super('directus_activity', options);
23
- this.notificationsService = new NotificationsService({ schema: this.schema });
24
- this.usersService = new UsersService({ schema: this.schema });
25
- }
26
- async createOne(data, opts) {
27
- if (data['action'] === Action.COMMENT && typeof data['comment'] === 'string') {
28
- const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
29
- const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
30
- const sender = await this.usersService.readOne(this.accountability.user, {
31
- fields: ['id', 'first_name', 'last_name', 'email'],
32
- });
33
- for (const mention of mentions) {
34
- const userID = mention.substring(1);
35
- const user = await this.usersService.readOne(userID, {
36
- fields: ['id', 'first_name', 'last_name', 'email', 'role'],
37
- });
38
- const roles = await fetchRolesTree(user['role'], this.knex);
39
- const globalAccess = await fetchGlobalAccess({ user: user['id'], roles, ip: null }, this.knex);
40
- const accountability = createDefaultAccountability({
41
- user: userID,
42
- role: user['role']?.id ?? null,
43
- roles,
44
- ...globalAccess,
45
- });
46
- const usersService = new UsersService({ schema: this.schema, accountability });
47
- try {
48
- if (this.accountability) {
49
- await validateAccess({
50
- accountability: this.accountability,
51
- action: 'read',
52
- collection: data['collection'],
53
- primaryKeys: [data['item']],
54
- }, {
55
- knex: this.knex,
56
- schema: this.schema,
57
- });
58
- }
59
- const templateData = await usersService.readByQuery({
60
- fields: ['id', 'first_name', 'last_name', 'email'],
61
- filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
62
- });
63
- const userPreviews = templateData.reduce((acc, user) => {
64
- acc[user['id']] = `<em>${userName(user)}</em>`;
65
- return acc;
66
- }, {});
67
- let comment = data['comment'];
68
- for (const mention of mentions) {
69
- const uuid = mention.substring(1);
70
- // We only match on UUIDs in the first place. This is just an extra sanity check.
71
- if (isValidUuid(uuid) === false)
72
- continue;
73
- comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
74
- }
75
- comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
76
- const href = new Url(env['PUBLIC_URL'])
77
- .addPath('admin', 'content', data['collection'], data['item'])
78
- .toString();
79
- const message = `
80
- Hello ${userName(user)},
81
-
82
- ${userName(sender)} has mentioned you in a comment:
83
-
84
- ${comment}
85
-
86
- <a href="${href}">Click here to view.</a>
87
- `;
88
- await this.notificationsService.createOne({
89
- recipient: userID,
90
- sender: sender['id'],
91
- subject: `You were mentioned in ${data['collection']}`,
92
- message,
93
- collection: data['collection'],
94
- item: data['item'],
95
- });
96
- }
97
- catch (err) {
98
- if (isDirectusError(err, ErrorCode.Forbidden)) {
99
- logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
100
- }
101
- else {
102
- throw err;
103
- }
104
- }
105
- }
106
- }
107
- return super.createOne(data, opts);
108
5
  }
109
6
  }
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Range, Stat } from '@directus/storage';
3
2
  import type { Accountability, SchemaOverview } from '@directus/types';
4
3
  import type { Knex } from 'knex';
@@ -98,7 +98,7 @@ export class AssetsService {
98
98
  }
99
99
  if (exists) {
100
100
  return {
101
- stream: await storage.location(file.storage).read(assetFilename, range),
101
+ stream: await storage.location(file.storage).read(assetFilename, { range }),
102
102
  file,
103
103
  stat: await storage.location(file.storage).stat(assetFilename),
104
104
  };
@@ -121,7 +121,8 @@ export class AssetsService {
121
121
  reason: 'Server too busy',
122
122
  });
123
123
  }
124
- const readStream = await storage.location(file.storage).read(file.filename_disk, range);
124
+ const version = file.modified_on !== undefined ? String(new Date(file.modified_on).getTime() / 1000) : undefined;
125
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
125
126
  const transformer = getSharpInstance();
126
127
  transformer.timeout({
127
128
  seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
@@ -151,13 +152,13 @@ export class AssetsService {
151
152
  }
152
153
  }
153
154
  return {
154
- stream: await storage.location(file.storage).read(assetFilename, range),
155
+ stream: await storage.location(file.storage).read(assetFilename, { range }),
155
156
  stat: await storage.location(file.storage).stat(assetFilename),
156
157
  file,
157
158
  };
158
159
  }
159
160
  else {
160
- const readStream = await storage.location(file.storage).read(file.filename_disk, range);
161
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range });
161
162
  const stat = await storage.location(file.storage).stat(file.filename_disk);
162
163
  return { stream: readStream, file, stat };
163
164
  }
@@ -155,10 +155,11 @@ export class CollectionsService {
155
155
  if (opts?.autoPurgeSystemCache !== false) {
156
156
  await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
157
157
  }
158
+ // Refresh the schema for subsequent reads
159
+ this.schema = await getSchema();
158
160
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
159
- const updatedSchema = await getSchema();
160
161
  for (const nestedActionEvent of nestedActionEvents) {
161
- nestedActionEvent.context.schema = updatedSchema;
162
+ nestedActionEvent.context.schema = this.schema;
162
163
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
163
164
  }
164
165
  }
@@ -196,10 +197,11 @@ export class CollectionsService {
196
197
  if (opts?.autoPurgeSystemCache !== false) {
197
198
  await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
198
199
  }
200
+ // Refresh the schema for subsequent reads
201
+ this.schema = await getSchema();
199
202
  if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
200
- const updatedSchema = await getSchema();
201
203
  for (const nestedActionEvent of nestedActionEvents) {
202
- nestedActionEvent.context.schema = updatedSchema;
204
+ nestedActionEvent.context.schema = this.schema;
203
205
  emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
204
206
  }
205
207
  }