@directus/api 22.0.0 → 22.1.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 (67) hide show
  1. package/dist/cli/commands/init/questions.d.ts +7 -6
  2. package/dist/cli/commands/init/questions.js +2 -2
  3. package/dist/cli/utils/create-env/index.d.ts +2 -2
  4. package/dist/cli/utils/create-env/index.js +3 -1
  5. package/dist/cli/utils/drivers.js +1 -1
  6. package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
  7. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
  8. package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
  9. package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
  10. package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
  11. package/dist/database/helpers/fn/types.d.ts +6 -3
  12. package/dist/database/helpers/fn/types.js +2 -2
  13. package/dist/database/helpers/index.d.ts +3 -3
  14. package/dist/database/index.d.ts +1 -1
  15. package/dist/database/index.js +2 -2
  16. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
  17. package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
  18. package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
  19. package/dist/database/migrations/20240806A-permissions-policies.js +18 -3
  20. package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
  21. package/dist/database/run-ast/lib/get-db-query.js +9 -5
  22. package/dist/database/run-ast/run-ast.d.ts +2 -2
  23. package/dist/database/run-ast/run-ast.js +14 -7
  24. package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
  25. package/dist/database/run-ast/utils/apply-case-when.js +2 -2
  26. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
  27. package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
  28. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
  29. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
  30. package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
  31. package/dist/permissions/lib/fetch-permissions.js +5 -39
  32. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
  33. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
  34. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
  35. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
  36. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
  37. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
  38. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
  39. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
  40. package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
  41. package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
  42. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
  43. package/dist/permissions/modules/process-payload/process-payload.js +4 -5
  44. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
  45. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
  46. package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
  47. package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
  48. package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
  49. package/dist/server.js +17 -4
  50. package/dist/services/fields.d.ts +1 -1
  51. package/dist/services/fields.js +22 -19
  52. package/dist/services/import-export.js +2 -2
  53. package/dist/services/items.js +1 -1
  54. package/dist/services/meta.js +8 -7
  55. package/dist/services/permissions.js +19 -19
  56. package/dist/types/database.d.ts +1 -1
  57. package/dist/utils/apply-query.d.ts +3 -3
  58. package/dist/utils/apply-query.js +25 -20
  59. package/dist/utils/get-address.d.ts +5 -0
  60. package/dist/utils/get-address.js +13 -0
  61. package/dist/utils/get-column.d.ts +8 -4
  62. package/dist/utils/get-column.js +10 -2
  63. package/dist/utils/sanitize-query.js +1 -1
  64. package/dist/utils/transaction.js +28 -11
  65. package/dist/websocket/controllers/graphql.js +2 -3
  66. package/dist/websocket/controllers/rest.js +2 -3
  67. package/package.json +17 -16
@@ -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, aliasMap) {
7
+ export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
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
@@ -32,6 +32,7 @@ export function getColumnPreprocessor(knex, schema, table, cases, aliasMap) {
32
32
  ...fieldNode.query,
33
33
  filter: joinFilterWithCases(fieldNode.query.filter, fieldNode.cases),
34
34
  },
35
+ permissions,
35
36
  cases: fieldNode.cases,
36
37
  });
37
38
  }
@@ -50,6 +51,7 @@ export function getColumnPreprocessor(knex, schema, table, cases, aliasMap) {
50
51
  cases,
51
52
  table,
52
53
  alias,
54
+ permissions,
53
55
  }, { knex, schema });
54
56
  }
55
57
  return column;
@@ -1,5 +1,5 @@
1
- import type { Filter, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { FieldNode, FunctionFieldNode, M2ONode, O2MNode } from '../../../types/index.js';
4
4
  import type { AliasMap } from '../../../utils/get-column-path.js';
5
- export declare function getInnerQueryColumnPreProcessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], aliasMap: AliasMap, aliasPrefix: string): (fieldNode: FieldNode | FunctionFieldNode | M2ONode | O2MNode) => Knex.Raw<string> | null;
5
+ export declare function getInnerQueryColumnPreProcessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, aliasPrefix: string): (fieldNode: FieldNode | FunctionFieldNode | M2ONode | O2MNode) => Knex.Raw<string> | null;
@@ -1,6 +1,6 @@
1
1
  import { applyCaseWhen } from './apply-case-when.js';
2
2
  import { getNodeAlias } from './get-field-alias.js';
3
- export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, aliasPrefix) {
3
+ export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, aliasPrefix) {
4
4
  return function (fieldNode) {
5
5
  const alias = getNodeAlias(fieldNode);
6
6
  if (fieldNode.whenCase && fieldNode.whenCase.length > 0) {
@@ -15,6 +15,7 @@ export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, alia
15
15
  aliasMap,
16
16
  cases,
17
17
  table,
18
+ permissions,
18
19
  }, { knex, schema });
19
20
  return knex.raw('COUNT(??) AS ??', [caseWhen, `${aliasPrefix}_${alias}`]);
20
21
  }
@@ -1,6 +1,5 @@
1
- import type { Accountability, Permission, PermissionsAction } from '@directus/types';
1
+ import type { Accountability, PermissionsAction } from '@directus/types';
2
2
  import type { Context } from '../types.js';
3
- export declare const fetchPermissions: typeof _fetchPermissions;
4
3
  export interface FetchPermissionsOptions {
5
4
  action?: PermissionsAction;
6
5
  policies: string[];
@@ -8,4 +7,4 @@ export interface FetchPermissionsOptions {
8
7
  accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
9
8
  bypassDynamicVariableProcessing?: boolean;
10
9
  }
11
- export declare function _fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<Permission[]>;
10
+ export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<import("@directus/types").Permission[]>;
@@ -1,51 +1,17 @@
1
- import { pick, sortBy } from 'lodash-es';
2
1
  import { fetchDynamicVariableContext } from '../utils/fetch-dynamic-variable-context.js';
2
+ import { fetchRawPermissions } from '../utils/fetch-raw-permissions.js';
3
3
  import { processPermissions } from '../utils/process-permissions.js';
4
- import { withCache } from '../utils/with-cache.js';
5
- import { withAppMinimalPermissions } from './with-app-minimal-permissions.js';
6
- export const fetchPermissions = withCache('permissions', _fetchPermissions, ({ action, policies, collections, accountability, bypassDynamicVariableProcessing }) => ({
7
- policies, // we assume that policies always come from the same source, so they should be in the same order
8
- ...(action && { action }),
9
- ...(collections && { collections: sortBy(collections) }),
10
- ...(accountability && { accountability: pick(accountability, ['user', 'role', 'roles', 'app']) }),
11
- ...(bypassDynamicVariableProcessing && { bypassDynamicVariableProcessing }),
12
- }));
13
- export async function _fetchPermissions(options, context) {
14
- const { PermissionsService } = await import('../../services/permissions.js');
15
- const permissionsService = new PermissionsService(context);
16
- const filter = {
17
- _and: [{ policy: { _in: options.policies } }],
18
- };
19
- if (options.action) {
20
- filter._and.push({ action: { _eq: options.action } });
21
- }
22
- if (options.collections) {
23
- filter._and.push({ collection: { _in: options.collections } });
24
- }
25
- let permissions = (await permissionsService.readByQuery({
26
- filter,
27
- limit: -1,
28
- }));
29
- // Sort permissions by their order in the policies array
30
- // This ensures that if a sorted array of policies is passed in the permissions are returned in the same order
31
- // which is necessary for correctly applying the presets in order
32
- permissions = sortBy(permissions, (permission) => options.policies.indexOf(permission.policy));
4
+ export async function fetchPermissions(options, context) {
5
+ const permissions = await fetchRawPermissions({ ...options, bypassMinimalAppPermissions: options.bypassDynamicVariableProcessing ?? false }, context);
33
6
  if (options.accountability && !options.bypassDynamicVariableProcessing) {
34
- // Add app minimal permissions for the request accountability, if applicable.
35
- // Normally this is done in the permissions service readByQuery, but it also needs to do it here
36
- // since the permissions service is created without accountability.
37
- // We call it without the policies filter, since the static minimal app permissions don't have a policy attached.
38
- const permissionsWithAppPermissions = withAppMinimalPermissions(options.accountability ?? null, permissions, {
39
- _and: filter._and.slice(1),
40
- });
41
7
  const permissionsContext = await fetchDynamicVariableContext({
42
8
  accountability: options.accountability,
43
9
  policies: options.policies,
44
- permissions: permissionsWithAppPermissions,
10
+ permissions,
45
11
  }, context);
46
12
  // Replace dynamic variables with their actual values
47
13
  const processedPermissions = processPermissions({
48
- permissions: permissionsWithAppPermissions,
14
+ permissions,
49
15
  accountability: options.accountability,
50
16
  permissionsContext,
51
17
  });
@@ -4,5 +4,4 @@ export interface FetchAllowedCollectionsOptions {
4
4
  action: PermissionsAction;
5
5
  accountability: Pick<Accountability, 'user' | 'role' | 'roles' | 'ip' | 'admin' | 'app'>;
6
6
  }
7
- export declare const fetchAllowedCollections: typeof _fetchAllowedCollections;
8
- export declare function _fetchAllowedCollections({ action, accountability }: FetchAllowedCollectionsOptions, { knex, schema }: Context): Promise<string[]>;
7
+ export declare function fetchAllowedCollections({ action, accountability }: FetchAllowedCollectionsOptions, { knex, schema }: Context): Promise<string[]>;
@@ -1,19 +1,7 @@
1
1
  import { uniq } from 'lodash-es';
2
2
  import { fetchPolicies } from '../../lib/fetch-policies.js';
3
- import { withCache } from '../../utils/with-cache.js';
4
3
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
5
- export const fetchAllowedCollections = withCache('allowed-collections', _fetchAllowedCollections, ({ action, accountability: { user, role, roles, ip, admin, app } }) => ({
6
- action,
7
- accountability: {
8
- user,
9
- role,
10
- roles,
11
- ip,
12
- admin,
13
- app,
14
- },
15
- }));
16
- export async function _fetchAllowedCollections({ action, accountability }, { knex, schema }) {
4
+ export async function fetchAllowedCollections({ action, accountability }, { knex, schema }) {
17
5
  if (accountability.admin) {
18
6
  return Object.keys(schema.collections);
19
7
  }
@@ -5,5 +5,4 @@ export interface FetchAllowedFieldMapOptions {
5
5
  accountability: Pick<Accountability, 'user' | 'role' | 'roles' | 'ip' | 'admin' | 'app'>;
6
6
  action: PermissionsAction;
7
7
  }
8
- export declare const fetchAllowedFieldMap: typeof _fetchAllowedFieldMap;
9
- export declare function _fetchAllowedFieldMap({ accountability, action }: FetchAllowedFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
8
+ export declare function fetchAllowedFieldMap({ accountability, action }: FetchAllowedFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
@@ -1,12 +1,7 @@
1
1
  import { uniq } from 'lodash-es';
2
2
  import { fetchPolicies } from '../../lib/fetch-policies.js';
3
- import { withCache } from '../../utils/with-cache.js';
4
3
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
5
- export const fetchAllowedFieldMap = withCache('allowed-field-map', _fetchAllowedFieldMap, ({ action, accountability: { user, role, roles, ip, admin, app } }) => ({
6
- action,
7
- accountability: { user, role, roles, ip, admin, app },
8
- }));
9
- export async function _fetchAllowedFieldMap({ accountability, action }, { knex, schema }) {
4
+ export async function fetchAllowedFieldMap({ accountability, action }, { knex, schema }) {
10
5
  const fieldMap = {};
11
6
  if (accountability.admin) {
12
7
  for (const [collection, { fields }] of Object.entries(schema.collections)) {
@@ -5,7 +5,6 @@ export interface FetchAllowedFieldsOptions {
5
5
  action: PermissionsAction;
6
6
  accountability: Pick<Accountability, 'user' | 'role' | 'roles' | 'ip' | 'app'>;
7
7
  }
8
- export declare const fetchAllowedFields: typeof _fetchAllowedFields;
9
8
  /**
10
9
  * Look up all fields that are allowed to be used for the given collection and action for the given
11
10
  * accountability object
@@ -13,4 +12,4 @@ export declare const fetchAllowedFields: typeof _fetchAllowedFields;
13
12
  * Done by looking up all available policies for the current accountability object, and reading all
14
13
  * permissions that exist for the collection+action+policy combination
15
14
  */
16
- export declare function _fetchAllowedFields({ accountability, action, collection }: FetchAllowedFieldsOptions, { knex, schema }: Context): Promise<string[]>;
15
+ export declare function fetchAllowedFields({ accountability, action, collection }: FetchAllowedFieldsOptions, { knex, schema }: Context): Promise<string[]>;
@@ -1,12 +1,6 @@
1
1
  import { uniq } from 'lodash-es';
2
2
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
3
3
  import { fetchPolicies } from '../../lib/fetch-policies.js';
4
- import { withCache } from '../../utils/with-cache.js';
5
- export const fetchAllowedFields = withCache('allowed-fields', _fetchAllowedFields, ({ action, collection, accountability: { user, role, roles, ip, app } }) => ({
6
- action,
7
- collection,
8
- accountability: { user, role, roles, ip, app },
9
- }));
10
4
  /**
11
5
  * Look up all fields that are allowed to be used for the given collection and action for the given
12
6
  * accountability object
@@ -14,7 +8,7 @@ export const fetchAllowedFields = withCache('allowed-fields', _fetchAllowedField
14
8
  * Done by looking up all available policies for the current accountability object, and reading all
15
9
  * permissions that exist for the collection+action+policy combination
16
10
  */
17
- export async function _fetchAllowedFields({ accountability, action, collection }, { knex, schema }) {
11
+ export async function fetchAllowedFields({ accountability, action, collection }, { knex, schema }) {
18
12
  const policies = await fetchPolicies(accountability, { knex, schema });
19
13
  const permissions = await fetchPermissions({ action, collections: [collection], policies, accountability }, { knex, schema });
20
14
  const allowedFields = [];
@@ -8,5 +8,4 @@ export interface FetchInconsistentFieldMapOptions {
8
8
  /**
9
9
  * Fetch a field map for fields that may or may not be null based on item-by-item permissions.
10
10
  */
11
- export declare const fetchInconsistentFieldMap: typeof _fetchInconsistentFieldMap;
12
- export declare function _fetchInconsistentFieldMap({ accountability, action }: FetchInconsistentFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
11
+ export declare function fetchInconsistentFieldMap({ accountability, action }: FetchInconsistentFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
@@ -1,15 +1,10 @@
1
- import { uniq, intersection, difference, pick } from 'lodash-es';
1
+ import { uniq, intersection, difference } from 'lodash-es';
2
2
  import { fetchPolicies } from '../../lib/fetch-policies.js';
3
- import { withCache } from '../../utils/with-cache.js';
4
3
  import { fetchPermissions } from '../../lib/fetch-permissions.js';
5
4
  /**
6
5
  * Fetch a field map for fields that may or may not be null based on item-by-item permissions.
7
6
  */
8
- export const fetchInconsistentFieldMap = withCache('inconsistent-field-map', _fetchInconsistentFieldMap, ({ action, accountability }) => ({
9
- action,
10
- accountability: accountability ? pick(accountability, ['user', 'role', 'roles', 'ip', 'admin', 'app']) : null,
11
- }));
12
- export async function _fetchInconsistentFieldMap({ accountability, action }, { knex, schema }) {
7
+ export async function fetchInconsistentFieldMap({ accountability, action }, { knex, schema }) {
13
8
  const fieldMap = {};
14
9
  if (!accountability || accountability.admin) {
15
10
  for (const collection of Object.keys(schema.collections)) {
@@ -0,0 +1,6 @@
1
+ import type { Filter, Permission } from '@directus/types';
2
+ export declare function getCases(collection: string, permissions: Permission[], requestedKeys: string[]): {
3
+ cases: Filter[];
4
+ caseMap: Record<string, number[]>;
5
+ allowedFields: Set<string>;
6
+ };
@@ -0,0 +1,40 @@
1
+ import { dedupeAccess } from '../utils/dedupe-access.js';
2
+ import { hasItemPermissions } from '../utils/has-item-permissions.js';
3
+ export function getCases(collection, permissions, requestedKeys) {
4
+ const permissionsForCollection = permissions.filter((permission) => permission.collection === collection);
5
+ const rules = dedupeAccess(permissionsForCollection);
6
+ const cases = [];
7
+ const caseMap = {};
8
+ // TODO this can be optimized if there is only one rule to skip the whole case/where system,
9
+ // since fields that are not allowed at all are already filtered out
10
+ // TODO this can be optimized if all cases are the same for all requested keys, as those should be
11
+ //
12
+ let index = 0;
13
+ for (const { rule, fields } of rules) {
14
+ // If none of the fields in the current permissions rule overlap with the actually requested
15
+ // fields in the AST, we can ignore this case altogether
16
+ if (requestedKeys.length > 0 &&
17
+ fields.has('*') === false &&
18
+ Array.from(fields).every((field) => requestedKeys.includes(field) === false)) {
19
+ continue;
20
+ }
21
+ if (rule === null)
22
+ continue;
23
+ cases.push(rule);
24
+ for (const field of fields) {
25
+ caseMap[field] = [...(caseMap[field] ?? []), index];
26
+ }
27
+ index++;
28
+ }
29
+ // Field that are allowed no matter what conditions exist for the item. These come from
30
+ // permissions where the item read access is "everything"
31
+ const allowedFields = new Set(permissionsForCollection
32
+ .filter((permission) => hasItemPermissions(permission) === false)
33
+ .map((permission) => permission.fields ?? [])
34
+ .flat());
35
+ return {
36
+ cases,
37
+ caseMap,
38
+ allowedFields,
39
+ };
40
+ }
@@ -1,7 +1,6 @@
1
1
  import { getUnaliasedFieldKey } from '../../../utils/get-unaliased-field-key.js';
2
- import { dedupeAccess } from '../utils/dedupe-access.js';
3
- import { hasItemPermissions } from '../utils/has-item-permissions.js';
4
2
  import { uniq } from 'lodash-es';
3
+ import { getCases } from './get-cases.js';
5
4
  /**
6
5
  * Mutates passed AST
7
6
  *
@@ -53,41 +52,3 @@ function processChildren(collection, children, permissions) {
53
52
  }
54
53
  return cases;
55
54
  }
56
- function getCases(collection, permissions, requestedKeys) {
57
- const permissionsForCollection = permissions.filter((permission) => permission.collection === collection);
58
- const rules = dedupeAccess(permissionsForCollection);
59
- const cases = [];
60
- const caseMap = {};
61
- // TODO this can be optimized if there is only one rule to skip the whole case/where system,
62
- // since fields that are not allowed at all are already filtered out
63
- // TODO this can be optimized if all cases are the same for all requested keys, as those should be
64
- //
65
- let index = 0;
66
- for (const { rule, fields } of rules) {
67
- // If none of the fields in the current permissions rule overlap with the actually requested
68
- // fields in the AST, we can ignore this case altogether
69
- if (requestedKeys.length > 0 &&
70
- fields.has('*') === false &&
71
- Array.from(fields).every((field) => requestedKeys.includes(field) === false)) {
72
- continue;
73
- }
74
- if (rule === null)
75
- continue;
76
- cases.push(rule);
77
- for (const field of fields) {
78
- caseMap[field] = [...(caseMap[field] ?? []), index];
79
- }
80
- index++;
81
- }
82
- // Field that are allowed no matter what conditions exist for the item. These come from
83
- // permissions where the item read access is "everything"
84
- const allowedFields = new Set(permissionsForCollection
85
- .filter((permission) => hasItemPermissions(permission) === false)
86
- .map((permission) => permission.fields ?? [])
87
- .flat());
88
- return {
89
- cases,
90
- caseMap,
91
- allowedFields,
92
- };
93
- }
@@ -55,6 +55,8 @@ export async function processPayload(options, context) {
55
55
  }
56
56
  fieldValidationRules.push(field.validation);
57
57
  }
58
+ const presets = (permissions ?? []).map((permission) => permission.presets);
59
+ const payloadWithPresets = assign({}, ...presets, options.payload);
58
60
  const validationRules = [...fieldValidationRules, ...permissionValidationRules].filter((rule) => {
59
61
  if (rule === null)
60
62
  return false;
@@ -64,14 +66,11 @@ export async function processPayload(options, context) {
64
66
  });
65
67
  if (validationRules.length > 0) {
66
68
  const validationErrors = [];
67
- validationErrors.push(...validatePayload({ _and: validationRules }, options.payload)
69
+ validationErrors.push(...validatePayload({ _and: validationRules }, payloadWithPresets)
68
70
  .map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
69
71
  .flat());
70
72
  if (validationErrors.length > 0)
71
73
  throw validationErrors;
72
74
  }
73
- if (!permissions)
74
- return options.payload;
75
- const presets = permissions.map((permission) => permission.presets);
76
- return assign({}, ...presets, options.payload);
75
+ return payloadWithPresets;
77
76
  }
@@ -13,11 +13,6 @@ export async function validateItemAccess(options, context) {
13
13
  // whole or not
14
14
  fields: [],
15
15
  limit: options.primaryKeys.length,
16
- filter: {
17
- [primaryKeyField]: {
18
- _in: options.primaryKeys,
19
- },
20
- },
21
16
  };
22
17
  const ast = await getAstFromQuery({
23
18
  accountability: options.accountability,
@@ -25,7 +20,13 @@ export async function validateItemAccess(options, context) {
25
20
  collection: options.collection,
26
21
  }, context);
27
22
  await processAst({ ast, ...options }, context);
28
- const items = await runAst(ast, context.schema, { knex: context.knex });
23
+ // Inject the filter after the permissions have been processed, as to not require access to the primary key
24
+ ast.query.filter = {
25
+ [primaryKeyField]: {
26
+ _in: options.primaryKeys,
27
+ },
28
+ };
29
+ const items = await runAst(ast, context.schema, options.accountability, { knex: context.knex });
29
30
  if (items && items.length === options.primaryKeys.length) {
30
31
  return true;
31
32
  }
@@ -1,9 +1,8 @@
1
1
  import type { Accountability, Permission } from '@directus/types';
2
2
  import type { Context } from '../types.js';
3
- export declare const fetchDynamicVariableContext: typeof _fetchDynamicVariableContext;
4
3
  export interface FetchDynamicVariableContext {
5
4
  accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
6
5
  policies: string[];
7
6
  permissions: Permission[];
8
7
  }
9
- export declare function _fetchDynamicVariableContext(options: FetchDynamicVariableContext, context: Context): Promise<Record<string, any>>;
8
+ export declare function fetchDynamicVariableContext(options: FetchDynamicVariableContext, context: Context): Promise<Record<string, any>>;
@@ -1,43 +1,63 @@
1
- import { extractRequiredDynamicVariableContext } from './extract-required-dynamic-variable-context.js';
2
- import { withCache } from './with-cache.js';
3
- export const fetchDynamicVariableContext = withCache('permission-dynamic-variables', _fetchDynamicVariableContext, ({ policies, permissions, accountability: { user, role, roles } }) => ({
4
- policies,
5
- permissions,
6
- accountability: {
7
- user,
8
- role,
9
- roles,
10
- },
11
- }));
12
- export async function _fetchDynamicVariableContext(options, context) {
1
+ import { useEnv } from '@directus/env';
2
+ import { getSimpleHash } from '@directus/utils';
3
+ import { getCache, getCacheValue, setCacheValue } from '../../cache.js';
4
+ import { extractRequiredDynamicVariableContext, } from './extract-required-dynamic-variable-context.js';
5
+ export async function fetchDynamicVariableContext(options, context) {
13
6
  const { UsersService } = await import('../../services/users.js');
14
7
  const { RolesService } = await import('../../services/roles.js');
15
8
  const { PoliciesService } = await import('../../services/policies.js');
16
9
  const contextData = {};
17
10
  const permissionContext = extractRequiredDynamicVariableContext(options.permissions);
18
11
  if (options.accountability.user && (permissionContext.$CURRENT_USER?.size ?? 0) > 0) {
19
- const usersService = new UsersService(context);
20
- contextData['$CURRENT_USER'] = await usersService.readOne(options.accountability.user, {
21
- fields: Array.from(permissionContext.$CURRENT_USER),
12
+ contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', permissionContext, { user: options.accountability.user }, async (fields) => {
13
+ const usersService = new UsersService(context);
14
+ return await usersService.readOne(options.accountability.user, {
15
+ fields,
16
+ });
22
17
  });
23
18
  }
24
19
  if (options.accountability.role && (permissionContext.$CURRENT_ROLE?.size ?? 0) > 0) {
25
- const rolesService = new RolesService(context);
26
- contextData['$CURRENT_ROLE'] = await rolesService.readOne(options.accountability.role, {
27
- fields: Array.from(permissionContext.$CURRENT_ROLE),
20
+ contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', permissionContext, { role: options.accountability.role }, async (fields) => {
21
+ const usersService = new RolesService(context);
22
+ return await usersService.readOne(options.accountability.role, {
23
+ fields,
24
+ });
28
25
  });
29
26
  }
30
27
  if (options.accountability.roles.length > 0 && (permissionContext.$CURRENT_ROLES?.size ?? 0) > 0) {
31
- const rolesService = new RolesService(context);
32
- contextData['$CURRENT_ROLES'] = await rolesService.readMany(options.accountability.roles, {
33
- fields: Array.from(permissionContext.$CURRENT_ROLES),
28
+ contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES', permissionContext, { roles: options.accountability.roles }, async (fields) => {
29
+ const rolesService = new RolesService(context);
30
+ return await rolesService.readMany(options.accountability.roles, {
31
+ fields,
32
+ });
34
33
  });
35
34
  }
36
35
  if (options.policies.length > 0 && (permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
37
- const policiesService = new PoliciesService(context);
38
- contextData['$CURRENT_POLICIES'] = await policiesService.readMany(options.policies, {
39
- fields: Array.from(permissionContext.$CURRENT_POLICIES),
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,
40
+ });
40
41
  });
41
42
  }
42
43
  return contextData;
43
44
  }
45
+ async function fetchContextData(key, permissionContext, cacheContext, fetch) {
46
+ const { cache } = getCache();
47
+ const env = useEnv();
48
+ const fields = Array.from(permissionContext[key]);
49
+ const cacheKey = cache
50
+ ? `filter-context-${key.slice(1)}-${getSimpleHash(JSON.stringify({ ...cacheContext, fields }))}`
51
+ : '';
52
+ let data = undefined;
53
+ if (cache) {
54
+ data = await getCacheValue(cache, cacheKey);
55
+ }
56
+ if (!data) {
57
+ data = await fetch(fields);
58
+ if (cache && env['CACHE_ENABLED'] !== false) {
59
+ await setCacheValue(cache, cacheKey, data);
60
+ }
61
+ }
62
+ return data;
63
+ }
@@ -0,0 +1,11 @@
1
+ import type { Accountability, Permission, PermissionsAction } from '@directus/types';
2
+ import type { Context } from '../types.js';
3
+ export declare const fetchRawPermissions: typeof _fetchRawPermissions;
4
+ export interface FetchRawPermissionsOptions {
5
+ action?: PermissionsAction;
6
+ policies: string[];
7
+ collections?: string[];
8
+ accountability?: Pick<Accountability, 'app'>;
9
+ bypassMinimalAppPermissions?: boolean;
10
+ }
11
+ export declare function _fetchRawPermissions(options: FetchRawPermissionsOptions, context: Context): Promise<Permission[]>;
@@ -0,0 +1,39 @@
1
+ import { sortBy } from 'lodash-es';
2
+ import { withAppMinimalPermissions } from '../lib/with-app-minimal-permissions.js';
3
+ import { withCache } from './with-cache.js';
4
+ export const fetchRawPermissions = withCache('raw-permissions', _fetchRawPermissions, ({ action, policies, collections, accountability, bypassMinimalAppPermissions }) => ({
5
+ policies, // we assume that policies always come from the same source, so they should be in the same order
6
+ ...(action && { action }),
7
+ ...(collections && { collections: sortBy(collections) }),
8
+ ...(accountability && { accountability: { app: accountability.app } }),
9
+ ...(bypassMinimalAppPermissions && { bypassMinimalAppPermissions }),
10
+ }));
11
+ export async function _fetchRawPermissions(options, context) {
12
+ const { PermissionsService } = await import('../../services/permissions.js');
13
+ const permissionsService = new PermissionsService(context);
14
+ const filter = {
15
+ _and: [{ policy: { _in: options.policies } }],
16
+ };
17
+ if (options.action) {
18
+ filter._and.push({ action: { _eq: options.action } });
19
+ }
20
+ if (options.collections) {
21
+ filter._and.push({ collection: { _in: options.collections } });
22
+ }
23
+ let permissions = (await permissionsService.readByQuery({
24
+ filter,
25
+ limit: -1,
26
+ }));
27
+ // Sort permissions by their order in the policies array
28
+ // This ensures that if a sorted array of policies is passed in the permissions are returned in the same order
29
+ // which is necessary for correctly applying the presets in order
30
+ permissions = sortBy(permissions, (permission) => options.policies.indexOf(permission.policy));
31
+ if (options.accountability && !options.bypassMinimalAppPermissions) {
32
+ // Add app minimal permissions for the request accountability, if applicable.
33
+ // Normally this is done in the permissions service readByQuery, but it also needs to do it here
34
+ // since the permissions service is created without accountability.
35
+ // We call it without the policies filter, since the static minimal app permissions don't have a policy attached.
36
+ return withAppMinimalPermissions(options.accountability ?? null, permissions, { _and: filter._and.slice(1) });
37
+ }
38
+ return permissions;
39
+ }
package/dist/server.js CHANGED
@@ -13,6 +13,7 @@ import emitter from './emitter.js';
13
13
  import { useLogger } from './logger/index.js';
14
14
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
15
15
  import { getIPFromReq } from './utils/get-ip-from-req.js';
16
+ import { getAddress } from './utils/get-address.js';
16
17
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
17
18
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
18
19
  export let SERVER_ONLINE = true;
@@ -116,10 +117,22 @@ export async function createServer() {
116
117
  export async function startServer() {
117
118
  const server = await createServer();
118
119
  const host = env['HOST'];
119
- const port = parseInt(env['PORT']);
120
+ const path = env['UNIX_SOCKET_PATH'];
121
+ const port = env['PORT'];
122
+ let listenOptions;
123
+ if (path) {
124
+ listenOptions = { path };
125
+ }
126
+ else {
127
+ listenOptions = {
128
+ host,
129
+ port: parseInt(port || '8055'),
130
+ };
131
+ }
120
132
  server
121
- .listen(port, host, () => {
122
- logger.info(`Server started at http://${host}:${port}`);
133
+ .listen(listenOptions, () => {
134
+ const protocol = server instanceof https.Server ? 'https' : 'http';
135
+ logger.info(`Server started at ${listenOptions.port ? `${protocol}://${getAddress(server)}` : getAddress(server)}`);
123
136
  process.send?.('ready');
124
137
  emitter.emitAction('server.start', { server }, {
125
138
  database: getDatabase(),
@@ -129,7 +142,7 @@ export async function startServer() {
129
142
  })
130
143
  .once('error', (err) => {
131
144
  if (err?.code === 'EADDRINUSE') {
132
- logger.error(`Port ${port} is already in use`);
145
+ logger.error(`${listenOptions.port ? `Port ${listenOptions.port}` : getAddress(server)} is already in use`);
133
146
  process.exit(1);
134
147
  }
135
148
  else {
@@ -30,5 +30,5 @@ export declare class FieldsService {
30
30
  updateField(collection: string, field: RawField, opts?: MutationOptions): Promise<string>;
31
31
  updateFields(collection: string, fields: RawField[], opts?: MutationOptions): Promise<string[]>;
32
32
  deleteField(collection: string, field: string, opts?: MutationOptions): Promise<void>;
33
- addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, alter?: Column | null): void;
33
+ addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, existing?: Column | null): void;
34
34
  }