@directus/api 22.1.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 (51) hide show
  1. package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
  2. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
  3. package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
  4. package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
  5. package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
  6. package/dist/database/helpers/fn/types.d.ts +6 -3
  7. package/dist/database/helpers/fn/types.js +2 -2
  8. package/dist/database/index.d.ts +1 -1
  9. package/dist/database/index.js +2 -2
  10. package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
  11. package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
  12. package/dist/database/run-ast/lib/get-db-query.js +9 -5
  13. package/dist/database/run-ast/run-ast.d.ts +2 -2
  14. package/dist/database/run-ast/run-ast.js +14 -7
  15. package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
  16. package/dist/database/run-ast/utils/apply-case-when.js +2 -2
  17. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
  18. package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
  19. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
  20. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
  21. package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
  22. package/dist/permissions/lib/fetch-permissions.js +5 -39
  23. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
  24. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
  25. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
  26. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
  27. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
  28. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
  29. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
  30. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
  31. package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
  32. package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
  33. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
  34. package/dist/permissions/modules/process-payload/process-payload.js +4 -5
  35. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
  36. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
  37. package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
  38. package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
  39. package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
  40. package/dist/services/fields.d.ts +1 -1
  41. package/dist/services/fields.js +22 -19
  42. package/dist/services/import-export.js +2 -2
  43. package/dist/services/items.js +1 -1
  44. package/dist/services/meta.js +8 -7
  45. package/dist/services/permissions.js +19 -19
  46. package/dist/utils/apply-query.d.ts +3 -3
  47. package/dist/utils/apply-query.js +25 -20
  48. package/dist/utils/get-column.d.ts +8 -4
  49. package/dist/utils/get-column.js +10 -2
  50. package/dist/utils/sanitize-query.js +1 -1
  51. package/package.json +15 -14
@@ -2,8 +2,8 @@
2
2
  * Generate an AST based on a given collection and query
3
3
  */
4
4
  import { cloneDeep, uniq } from 'lodash-es';
5
- import { fetchAllowedFields } from '../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
6
5
  import { parseFields } from './lib/parse-fields.js';
6
+ import { getAllowedSort } from './utils/get-allowed-sort.js';
7
7
  export async function getAstFromQuery(options, context) {
8
8
  options.query = cloneDeep(options.query);
9
9
  const ast = {
@@ -36,36 +36,7 @@ export async function getAstFromQuery(options, context) {
36
36
  // Prevent fields/deep from showing up in the query object in further use
37
37
  delete options.query.fields;
38
38
  delete options.query.deep;
39
- if (!options.query.sort) {
40
- // We'll default to the primary key for the standard sort output
41
- let sortField = context.schema.collections[options.collection].primary;
42
- // If a custom manual sort field is configured, use that
43
- if (context.schema.collections[options.collection]?.sortField) {
44
- sortField = context.schema.collections[options.collection].sortField;
45
- }
46
- if (options.accountability && options.accountability.admin === false) {
47
- // Verify that the user has access to the sort field
48
- const allowedFields = await fetchAllowedFields({
49
- collection: options.collection,
50
- action: 'read',
51
- accountability: options.accountability,
52
- }, context);
53
- if (allowedFields.length === 0) {
54
- sortField = null;
55
- }
56
- else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
57
- // If the sort field is not allowed, default to the first allowed field
58
- sortField = allowedFields[0];
59
- }
60
- }
61
- // When group by is used, default to the first column provided in the group by clause
62
- if (options.query.group?.[0]) {
63
- sortField = options.query.group[0];
64
- }
65
- if (sortField) {
66
- options.query.sort = [sortField];
67
- }
68
- }
39
+ options.query.sort ??= await getAllowedSort(options, context);
69
40
  // When no group by is supplied, but an aggregate function is used, only a single row will be
70
41
  // returned. In those cases, we'll ignore the sort field altogether
71
42
  if (options.query.aggregate && Object.keys(options.query.aggregate).length && !options.query.group?.[0]) {
@@ -1,6 +1,6 @@
1
1
  import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
- import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../../../types/index.js';
3
+ import type { FieldNode, FunctionFieldNode, NestedCollectionNode, O2MNode } from '../../../types/index.js';
4
4
  export interface ParseFieldsOptions {
5
5
  accountability: Accountability | null;
6
6
  parentCollection: string;
@@ -13,3 +13,4 @@ export interface ParseFieldsContext {
13
13
  knex: Knex;
14
14
  }
15
15
  export declare function parseFields(options: ParseFieldsOptions, context: ParseFieldsContext): Promise<[] | (NestedCollectionNode | FieldNode | FunctionFieldNode)[]>;
16
+ export declare function isO2MNode(node: NestedCollectionNode | null): node is O2MNode;
@@ -3,6 +3,7 @@ import { isEmpty } from 'lodash-es';
3
3
  import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
4
4
  import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
5
5
  import { getRelationType } from '../../../utils/get-relation-type.js';
6
+ import { getAllowedSort } from '../utils/get-allowed-sort.js';
6
7
  import { getDeepQuery } from '../utils/get-deep-query.js';
7
8
  import { getRelatedCollection } from '../utils/get-related-collection.js';
8
9
  import { getRelation } from '../utils/get-relation.js';
@@ -119,7 +120,16 @@ export async function parseFields(options, context) {
119
120
  continue;
120
121
  let child = null;
121
122
  if (relationType === 'a2o') {
122
- const allowedCollections = relation.meta.one_allowed_collections;
123
+ let allowedCollections = relation.meta.one_allowed_collections;
124
+ if (options.accountability && options.accountability.admin === false && policies) {
125
+ const permissions = await fetchPermissions({
126
+ action: 'read',
127
+ collections: allowedCollections,
128
+ policies: policies,
129
+ accountability: options.accountability,
130
+ }, context);
131
+ allowedCollections = allowedCollections.filter((collection) => permissions.some((permission) => permission.collection === collection));
132
+ }
123
133
  child = {
124
134
  type: 'a2o',
125
135
  names: allowedCollections,
@@ -181,8 +191,13 @@ export async function parseFields(options, context) {
181
191
  cases: [],
182
192
  whenCase: [],
183
193
  };
184
- if (relationType === 'o2m' && !child.query.sort) {
185
- child.query.sort = [relation.meta?.sort_field || context.schema.collections[relation.collection].primary];
194
+ if (isO2MNode(child) && !child.query.sort) {
195
+ child.query.sort = await getAllowedSort({ collection: relation.collection, relation, accountability: options.accountability }, context);
196
+ }
197
+ if (isO2MNode(child) && child.query.group && child.query.group[0] !== relation.field) {
198
+ // If a group by is used, the result needs to be grouped by the foreign key of the relation first, so results
199
+ // are correctly grouped under the foreign key when extracting the grouped results from the nested queries.
200
+ child.query.group.unshift(relation.field);
186
201
  }
187
202
  }
188
203
  if (child) {
@@ -198,3 +213,6 @@ export async function parseFields(options, context) {
198
213
  return true;
199
214
  });
200
215
  }
216
+ export function isO2MNode(node) {
217
+ return !!node && node.type === 'o2m';
218
+ }
@@ -0,0 +1,9 @@
1
+ import type { Accountability, Query, Relation } from '@directus/types';
2
+ import type { Context } from '../../../permissions/types.js';
3
+ export type GetAllowedSortFieldOptions = {
4
+ collection: string;
5
+ accountability: Accountability | null;
6
+ query?: Query;
7
+ relation?: Relation;
8
+ };
9
+ export declare function getAllowedSort(options: GetAllowedSortFieldOptions, context: Context): Promise<string[] | null>;
@@ -0,0 +1,35 @@
1
+ import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
2
+ export async function getAllowedSort(options, context) {
3
+ // We'll default to the primary key for the standard sort output
4
+ let sortField = context.schema.collections[options.collection].primary;
5
+ // If a custom manual sort field is configured, use that
6
+ if (context.schema.collections[options.collection]?.sortField) {
7
+ sortField = context.schema.collections[options.collection].sortField;
8
+ }
9
+ // If a sort field is defined on the relation, use that
10
+ if (options.relation?.meta?.sort_field) {
11
+ sortField = options.relation.meta.sort_field;
12
+ }
13
+ if (options.accountability && options.accountability.admin === false) {
14
+ // Verify that the user has access to the sort field
15
+ const allowedFields = await fetchAllowedFields({
16
+ collection: options.collection,
17
+ action: 'read',
18
+ accountability: options.accountability,
19
+ }, context);
20
+ if (allowedFields.length === 0) {
21
+ sortField = null;
22
+ }
23
+ else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
24
+ // If the sort field is not allowed, default to the first allowed field
25
+ sortField = allowedFields[0];
26
+ }
27
+ }
28
+ // When group by is used, default to the first column provided in the group by clause
29
+ if (options.query?.group?.[0]) {
30
+ sortField = options.query.group[0];
31
+ }
32
+ if (sortField)
33
+ return [sortField];
34
+ return null;
35
+ }
@@ -1,11 +1,14 @@
1
- import type { Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import { DatabaseHelper } from '../types.js';
4
4
  export type FnHelperOptions = {
5
5
  type: string | undefined;
6
- query: Query | undefined;
7
- cases: Filter[] | undefined;
8
6
  originalCollectionName: string | undefined;
7
+ relationalCountOptions: {
8
+ query: Query;
9
+ cases: Filter[];
10
+ permissions: Permission[];
11
+ } | undefined;
9
12
  };
10
13
  export declare abstract class FnHelper extends DatabaseHelper {
11
14
  protected schema: SchemaOverview;
@@ -20,7 +20,7 @@ export class FnHelper extends DatabaseHelper {
20
20
  .count('*')
21
21
  .from({ [alias]: relation.collection })
22
22
  .where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));
23
- if (options?.query?.filter) {
23
+ if (options?.relationalCountOptions?.query.filter) {
24
24
  // set the newly aliased collection in the alias map as the default parent collection, indicated by '', for any nested filters
25
25
  const aliasMap = {
26
26
  '': {
@@ -28,7 +28,7 @@ export class FnHelper extends DatabaseHelper {
28
28
  collection: relation.collection,
29
29
  },
30
30
  };
31
- countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, aliasMap, options.cases ?? []).query;
31
+ countQuery = applyFilter(this.knex, this.schema, countQuery, options.relationalCountOptions.query.filter, relation.collection, aliasMap, options.relationalCountOptions.cases, options.relationalCountOptions.permissions).query;
32
32
  }
33
33
  return this.knex.raw('(' + countQuery.toQuery() + ')');
34
34
  }
@@ -3,7 +3,7 @@ import type { Knex } from 'knex';
3
3
  import type { DatabaseClient } from '../types/index.js';
4
4
  export default getDatabase;
5
5
  export declare function getDatabase(): Knex;
6
- export declare function getSchemaInspector(): SchemaInspector;
6
+ export declare function getSchemaInspector(database?: Knex): SchemaInspector;
7
7
  /**
8
8
  * Get database version. Value currently exists for MySQL only.
9
9
  *
@@ -143,11 +143,11 @@ export function getDatabase() {
143
143
  });
144
144
  return database;
145
145
  }
146
- export function getSchemaInspector() {
146
+ export function getSchemaInspector(database) {
147
147
  if (inspector) {
148
148
  return inspector;
149
149
  }
150
- const database = getDatabase();
150
+ database ??= getDatabase();
151
151
  inspector = createInspector(database);
152
152
  return inspector;
153
153
  }
@@ -186,10 +186,10 @@ export async function up(knex) {
186
186
  /////////////////////////////////////////////////////////////////////////////////////////////////
187
187
  // Link permissions to policies instead of roles
188
188
  await knex.schema.alterTable('directus_permissions', (table) => {
189
- table.uuid('policy').references('directus_policies.id').onDelete('CASCADE');
189
+ table.uuid('policy');
190
190
  });
191
191
  try {
192
- const inspector = await getSchemaInspector();
192
+ const inspector = await getSchemaInspector(knex);
193
193
  const foreignKeys = await inspector.foreignKeys('directus_permissions');
194
194
  const foreignConstraint = foreignKeys.find((foreign) => foreign.foreign_key_table === 'directus_roles' && foreign.column === 'role')
195
195
  ?.constraint_name || undefined;
@@ -212,6 +212,7 @@ export async function up(knex) {
212
212
  await knex.schema.alterTable('directus_permissions', (table) => {
213
213
  table.dropColumns('role');
214
214
  table.dropNullable('policy');
215
+ table.foreign('policy').references('directus_policies.id').onDelete('CASCADE');
215
216
  });
216
217
  /////////////////////////////////////////////////////////////////////////////////////////////////
217
218
  // Setup junction table between roles/users and policies
@@ -1,4 +1,4 @@
1
- import type { Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  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[]): Knex.QueryBuilder;
4
+ export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): 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) {
12
+ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
13
13
  const aliasMap = Object.create(null);
14
14
  const env = useEnv();
15
- const preProcess = getColumnPreprocessor(knex, schema, table, cases, aliasMap);
15
+ const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
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) ||
@@ -25,7 +25,10 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
25
25
  const groupWhenCases = hasCaseWhen
26
26
  ? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
27
27
  : undefined;
28
- const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, { aliasMap, groupWhenCases }).query;
28
+ const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
29
+ aliasMap,
30
+ groupWhenCases,
31
+ }).query;
29
32
  flatQuery.select(fieldNodes.map((node) => preProcess(node)));
30
33
  withPreprocessBindings(knex, dbQuery);
31
34
  return dbQuery;
@@ -42,7 +45,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
42
45
  hasMultiRelationalSort = sortResult.hasMultiRelationalSort;
43
46
  }
44
47
  }
45
- const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, {
48
+ const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, permissions, {
46
49
  aliasMap,
47
50
  isInnerQuery: true,
48
51
  hasMultiRelationalSort,
@@ -68,6 +71,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
68
71
  cases,
69
72
  table,
70
73
  alias: node.fieldKey,
74
+ permissions,
71
75
  }, { knex, schema });
72
76
  }));
73
77
  }
@@ -159,7 +163,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
159
163
  SELECT ...,
160
164
  CASE WHEN `inner`.<random-prefix>_<alias> > 0 THEN <actual-column> END AS <alias>
161
165
  */
162
- const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, innerCaseWhenAliasPrefix);
166
+ const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, innerCaseWhenAliasPrefix);
163
167
  // To optimize the query we avoid having unnecessary columns in the inner query, that don't have a caseWhen, since
164
168
  // they are selected in the outer query directly
165
169
  dbQuery.select(fieldNodes.map(innerPreprocess).filter((x) => x !== null));
@@ -1,7 +1,7 @@
1
- import type { Item, SchemaOverview } from '@directus/types';
1
+ import type { Accountability, Item, SchemaOverview } from '@directus/types';
2
2
  import type { AST, NestedCollectionNode } from '../../types/ast.js';
3
3
  import type { RunASTOptions } from './types.js';
4
4
  /**
5
5
  * Execute a given AST using Knex. Returns array of items based on requested AST.
6
6
  */
7
- export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, options?: RunASTOptions): Promise<null | Item | Item[]>;
7
+ export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, accountability: Accountability | null, options?: RunASTOptions): Promise<null | Item | Item[]>;
@@ -1,5 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { cloneDeep, merge } from 'lodash-es';
3
+ import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
4
+ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
3
5
  import { PayloadService } from '../../services/payload.js';
4
6
  import getDatabase from '../index.js';
5
7
  import { getDBQuery } from './lib/get-db-query.js';
@@ -10,26 +12,31 @@ import { removeTemporaryFields } from './utils/remove-temporary-fields.js';
10
12
  /**
11
13
  * Execute a given AST using Knex. Returns array of items based on requested AST.
12
14
  */
13
- export async function runAst(originalAST, schema, options) {
15
+ export async function runAst(originalAST, schema, accountability, options) {
14
16
  const ast = cloneDeep(originalAST);
15
17
  const knex = options?.knex || getDatabase();
16
18
  if (ast.type === 'a2o') {
17
19
  const results = {};
18
20
  for (const collection of ast.names) {
19
- results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? []);
21
+ results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? [], accountability);
20
22
  }
21
23
  return results;
22
24
  }
23
25
  else {
24
- return await run(ast.name, ast.children, options?.query || ast.query, ast.cases);
26
+ return await run(ast.name, ast.children, options?.query || ast.query, ast.cases, accountability);
25
27
  }
26
- async function run(collection, children, query, cases) {
28
+ async function run(collection, children, query, cases, accountability) {
27
29
  const env = useEnv();
28
30
  // Retrieve the database columns to select in the current AST
29
31
  const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(schema, collection, children, query);
30
32
  const o2mNodes = nestedCollectionNodes.filter((node) => node.type === 'o2m');
33
+ let permissions = [];
34
+ if (accountability && !accountability.admin) {
35
+ const policies = await fetchPolicies(accountability, { schema, knex });
36
+ permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
37
+ }
31
38
  // The actual knex query builder instance. This is a promise that resolves with the raw items from the db
32
- const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases);
39
+ const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
33
40
  const rawItems = await dbQuery;
34
41
  if (!rawItems)
35
42
  return null;
@@ -72,7 +79,7 @@ export async function runAst(originalAST, schema, options) {
72
79
  page: null,
73
80
  },
74
81
  });
75
- nestedItems = (await runAst(node, schema, { knex, nested: true }));
82
+ nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
76
83
  if (nestedItems) {
77
84
  items = mergeWithParentItems(schema, nestedItems, items, nestedNode, fieldAllowed);
78
85
  }
@@ -86,7 +93,7 @@ export async function runAst(originalAST, schema, options) {
86
93
  const node = merge({}, nestedNode, {
87
94
  query: { limit: -1 },
88
95
  });
89
- nestedItems = (await runAst(node, schema, { knex, nested: true }));
96
+ nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
90
97
  if (nestedItems) {
91
98
  // Merge all fetched nested records with the parent items
92
99
  items = mergeWithParentItems(schema, nestedItems, items, nestedNode, true);
@@ -1,4 +1,4 @@
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 { AliasMap } from '../../../utils/get-column-path.js';
4
4
  export interface ApplyCaseWhenOptions {
@@ -8,9 +8,10 @@ export interface ApplyCaseWhenOptions {
8
8
  cases: Filter[];
9
9
  aliasMap: AliasMap;
10
10
  alias?: string;
11
+ permissions: Permission[];
11
12
  }
12
13
  export interface ApplyCaseWhenContext {
13
14
  knex: Knex;
14
15
  schema: SchemaOverview;
15
16
  }
16
- export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
17
+ export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
@@ -1,7 +1,7 @@
1
1
  import { applyFilter } from '../../../utils/apply-query.js';
2
- export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }, { knex, schema }) {
2
+ export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }, { knex, schema }) {
3
3
  const caseQuery = knex.queryBuilder();
4
- applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases);
4
+ applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases, permissions);
5
5
  const compiler = knex.client.queryCompiler(caseQuery);
6
6
  const sqlParts = [];
7
7
  // Only empty filters, so no where was generated, skip it
@@ -1,4 +1,4 @@
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 } from '../../../types/ast.js';
4
4
  import type { AliasMap } from '../../../utils/get-column-path.js';
@@ -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[], 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): (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, 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[]>;