directus 9.3.0 → 9.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/app.js +25 -4
  2. package/dist/auth/auth.d.ts +4 -6
  3. package/dist/auth/auth.js +5 -9
  4. package/dist/auth/drivers/ldap.d.ts +3 -3
  5. package/dist/auth/drivers/ldap.js +2 -3
  6. package/dist/auth/drivers/local.d.ts +2 -2
  7. package/dist/auth/drivers/local.js +7 -13
  8. package/dist/auth/drivers/oauth2.d.ts +3 -3
  9. package/dist/auth/drivers/oauth2.js +4 -4
  10. package/dist/auth/drivers/openid.d.ts +3 -3
  11. package/dist/auth/drivers/openid.js +4 -4
  12. package/dist/cache.js +1 -3
  13. package/dist/cli/commands/schema/apply.js +1 -1
  14. package/dist/cli/index.js +1 -1
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.js +16 -2
  17. package/dist/controllers/activity.js +2 -1
  18. package/dist/controllers/auth.js +5 -4
  19. package/dist/controllers/extensions.js +1 -1
  20. package/dist/controllers/shares.d.ts +2 -0
  21. package/dist/controllers/shares.js +212 -0
  22. package/dist/controllers/users.js +21 -9
  23. package/dist/database/index.js +3 -0
  24. package/dist/database/migrations/20211211A-add-shares.d.ts +3 -0
  25. package/dist/database/migrations/20211211A-add-shares.js +38 -0
  26. package/dist/database/migrations/20211230A-add-project-descriptor.d.ts +3 -0
  27. package/dist/database/migrations/20211230A-add-project-descriptor.js +15 -0
  28. package/dist/database/migrations/run.js +1 -1
  29. package/dist/database/run-ast.js +5 -5
  30. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -15
  31. package/dist/database/system-data/app-access-permissions/index.d.ts +1 -0
  32. package/dist/database/system-data/app-access-permissions/index.js +4 -2
  33. package/dist/database/system-data/app-access-permissions/schema-access-permissions.yaml +17 -0
  34. package/dist/database/system-data/collections/collections.yaml +3 -0
  35. package/dist/database/system-data/fields/_defaults.yaml +2 -0
  36. package/dist/database/system-data/fields/sessions.yaml +1 -1
  37. package/dist/database/system-data/fields/settings.yaml +20 -1
  38. package/dist/database/system-data/fields/shares.yaml +77 -0
  39. package/dist/database/system-data/fields/users.yaml +1 -1
  40. package/dist/database/system-data/relations/relations.yaml +15 -0
  41. package/dist/env.js +4 -1
  42. package/dist/extensions.d.ts +11 -6
  43. package/dist/extensions.js +97 -42
  44. package/dist/middleware/authenticate.js +7 -16
  45. package/dist/middleware/check-ip.js +9 -6
  46. package/dist/middleware/rate-limiter.js +2 -1
  47. package/dist/middleware/respond.js +4 -1
  48. package/dist/services/activity.d.ts +2 -1
  49. package/dist/services/activity.js +2 -2
  50. package/dist/services/assets.js +3 -3
  51. package/dist/services/authentication.d.ts +2 -7
  52. package/dist/services/authentication.js +84 -41
  53. package/dist/services/authorization.js +3 -3
  54. package/dist/services/collections.d.ts +1 -2
  55. package/dist/services/collections.js +2 -2
  56. package/dist/services/files.d.ts +2 -2
  57. package/dist/services/graphql.d.ts +1 -1
  58. package/dist/services/graphql.js +51 -10
  59. package/dist/services/index.d.ts +1 -0
  60. package/dist/services/index.js +1 -0
  61. package/dist/services/items.d.ts +1 -15
  62. package/dist/services/notifications.d.ts +2 -2
  63. package/dist/services/permissions.d.ts +2 -2
  64. package/dist/services/roles.d.ts +2 -2
  65. package/dist/services/server.js +1 -0
  66. package/dist/services/shares.d.ts +17 -0
  67. package/dist/services/shares.js +135 -0
  68. package/dist/services/specifications.js +1 -1
  69. package/dist/services/users.d.ts +2 -2
  70. package/dist/services/webhooks.d.ts +2 -2
  71. package/dist/types/ast.d.ts +3 -3
  72. package/dist/types/auth.d.ts +31 -0
  73. package/dist/types/items.d.ts +14 -0
  74. package/dist/utils/apply-query.d.ts +0 -38
  75. package/dist/utils/apply-query.js +66 -67
  76. package/dist/utils/get-ast-from-query.js +3 -3
  77. package/dist/utils/get-default-value.js +3 -1
  78. package/dist/utils/get-ip-from-req.d.ts +2 -0
  79. package/dist/utils/get-ip-from-req.js +24 -0
  80. package/dist/utils/get-local-type.js +1 -1
  81. package/dist/utils/get-permissions.js +15 -7
  82. package/dist/utils/get-relation-type.d.ts +1 -1
  83. package/dist/utils/get-relation-type.js +1 -1
  84. package/dist/utils/merge-permissions-for-share.d.ts +5 -0
  85. package/dist/utils/merge-permissions-for-share.js +116 -0
  86. package/dist/utils/merge-permissions.d.ts +13 -1
  87. package/dist/utils/merge-permissions.js +27 -19
  88. package/dist/utils/reduce-schema.d.ts +2 -2
  89. package/dist/utils/reduce-schema.js +7 -7
  90. package/dist/utils/user-name.js +3 -0
  91. package/example.env +1 -1
  92. package/package.json +15 -13
@@ -1,5 +1,5 @@
1
- import { AbstractServiceOptions, Item, PrimaryKey, Webhook } from '../types';
2
- import { ItemsService, MutationOptions } from './items';
1
+ import { AbstractServiceOptions, Item, PrimaryKey, Webhook, MutationOptions } from '../types';
2
+ import { ItemsService } from './items';
3
3
  export declare class WebhooksService extends ItemsService<Webhook> {
4
4
  constructor(options: AbstractServiceOptions);
5
5
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
@@ -10,8 +10,8 @@ export declare type M2ONode = {
10
10
  parentKey: string;
11
11
  relatedKey: string;
12
12
  };
13
- export declare type M2ANode = {
14
- type: 'm2a';
13
+ export declare type A2MNode = {
14
+ type: 'a2o';
15
15
  names: string[];
16
16
  children: {
17
17
  [collection: string]: (NestedCollectionNode | FieldNode)[];
@@ -36,7 +36,7 @@ export declare type O2MNode = {
36
36
  parentKey: string;
37
37
  relatedKey: string;
38
38
  };
39
- export declare type NestedCollectionNode = M2ONode | O2MNode | M2ANode;
39
+ export declare type NestedCollectionNode = M2ONode | O2MNode | A2MNode;
40
40
  export declare type FieldNode = {
41
41
  type: 'field';
42
42
  name: string;
@@ -15,11 +15,42 @@ export interface User {
15
15
  provider: string;
16
16
  external_identifier: string | null;
17
17
  auth_data: string | Record<string, unknown> | null;
18
+ app_access: boolean;
19
+ admin_access: boolean;
18
20
  }
19
21
  export declare type AuthData = Record<string, any> | null;
20
22
  export interface Session {
21
23
  token: string;
22
24
  expires: Date;
23
25
  data: string | Record<string, unknown> | null;
26
+ share: string;
24
27
  }
25
28
  export declare type SessionData = Record<string, any> | null;
29
+ export declare type DirectusTokenPayload = {
30
+ id?: string;
31
+ role: string | null;
32
+ app_access: boolean | number;
33
+ admin_access: boolean | number;
34
+ share?: string;
35
+ share_scope?: {
36
+ collection: string;
37
+ item: string;
38
+ };
39
+ };
40
+ export declare type ShareData = {
41
+ share_id: string;
42
+ share_role: string;
43
+ share_item: string;
44
+ share_collection: string;
45
+ share_start: Date;
46
+ share_end: Date;
47
+ share_times_used: number;
48
+ share_max_uses?: number;
49
+ share_password?: string;
50
+ };
51
+ export declare type LoginResult = {
52
+ accessToken: any;
53
+ refreshToken: any;
54
+ expires: any;
55
+ id?: any;
56
+ };
@@ -13,3 +13,17 @@ export declare type Alterations = {
13
13
  }[];
14
14
  delete: (number | string)[];
15
15
  };
16
+ export declare type MutationOptions = {
17
+ /**
18
+ * Callback function that's fired whenever a revision is made in the mutation
19
+ */
20
+ onRevisionCreate?: (pk: PrimaryKey) => void;
21
+ /**
22
+ * Flag to disable the auto purging of the cache. Is ignored when CACHE_AUTO_PURGE isn't enabled.
23
+ */
24
+ autoPurgeCache?: false;
25
+ /**
26
+ * Allow disabling the emitting of hooks. Useful if a custom hook is fired (like files.upload)
27
+ */
28
+ emitEvents?: boolean;
29
+ };
@@ -5,44 +5,6 @@ import { Aggregate, Filter, Query } from '@directus/shared/types';
5
5
  * Apply the Query to a given Knex query builder instance
6
6
  */
7
7
  export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, subQuery?: boolean): Knex.QueryBuilder;
8
- /**
9
- * Apply a given filter object to the Knex QueryBuilder instance.
10
- *
11
- * Relational nested filters, like the following example:
12
- *
13
- * ```json
14
- * // Fetch pages that have articles written by Rijk
15
- *
16
- * {
17
- * "articles": {
18
- * "author": {
19
- * "name": {
20
- * "_eq": "Rijk"
21
- * }
22
- * }
23
- * }
24
- * }
25
- * ```
26
- *
27
- * are handled by joining the nested tables, and using a where statement on the top level on the
28
- * nested field through the join. This allows us to filter the top level items based on nested data.
29
- * The where on the root is done with a subquery to prevent duplicates, any nested joins are done
30
- * with aliases to prevent naming conflicts.
31
- *
32
- * The output SQL for the above would look something like:
33
- *
34
- * ```sql
35
- * SELECT *
36
- * FROM pages
37
- * WHERE
38
- * pages.id in (
39
- * SELECT articles.page_id AS page_id
40
- * FROM articles
41
- * LEFT JOIN authors AS xviqp ON articles.author = xviqp.id
42
- * WHERE xviqp.name = 'Rijk'
43
- * )
44
- * ```
45
- */
46
8
  export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, subQuery?: boolean): Knex.QueryBuilder<any, any>;
47
9
  export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
48
10
  export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
@@ -76,44 +76,44 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
76
76
  return dbQuery;
77
77
  }
78
78
  exports.default = applyQuery;
79
- /**
80
- * Apply a given filter object to the Knex QueryBuilder instance.
81
- *
82
- * Relational nested filters, like the following example:
83
- *
84
- * ```json
85
- * // Fetch pages that have articles written by Rijk
86
- *
87
- * {
88
- * "articles": {
89
- * "author": {
90
- * "name": {
91
- * "_eq": "Rijk"
92
- * }
93
- * }
94
- * }
95
- * }
96
- * ```
97
- *
98
- * are handled by joining the nested tables, and using a where statement on the top level on the
99
- * nested field through the join. This allows us to filter the top level items based on nested data.
100
- * The where on the root is done with a subquery to prevent duplicates, any nested joins are done
101
- * with aliases to prevent naming conflicts.
102
- *
103
- * The output SQL for the above would look something like:
104
- *
105
- * ```sql
106
- * SELECT *
107
- * FROM pages
108
- * WHERE
109
- * pages.id in (
110
- * SELECT articles.page_id AS page_id
111
- * FROM articles
112
- * LEFT JOIN authors AS xviqp ON articles.author = xviqp.id
113
- * WHERE xviqp.name = 'Rijk'
114
- * )
115
- * ```
116
- */
79
+ function getRelationInfo(relations, collection, field) {
80
+ var _a, _b;
81
+ const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
82
+ if (implicitRelation) {
83
+ if (implicitRelation[2] === undefined) {
84
+ const [m2oCollection, m2oField] = implicitRelation;
85
+ const relation = {
86
+ collection: m2oCollection,
87
+ field: m2oField,
88
+ related_collection: collection,
89
+ schema: null,
90
+ meta: null,
91
+ };
92
+ return { relation, relationType: 'o2m' };
93
+ }
94
+ else {
95
+ const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
96
+ const relation = {
97
+ collection: a2oCollection,
98
+ field: a2oItemField,
99
+ related_collection: collection,
100
+ schema: null,
101
+ meta: {
102
+ one_collection_field: a2oCollectionField,
103
+ one_field: field,
104
+ },
105
+ };
106
+ return { relation, relationType: 'o2a' };
107
+ }
108
+ }
109
+ const relation = (_b = relations.find((relation) => {
110
+ var _a;
111
+ return ((relation.collection === collection && relation.field === field) ||
112
+ (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
113
+ })) !== null && _b !== void 0 ? _b : null;
114
+ const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
115
+ return { relation, relationType };
116
+ }
117
117
  function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
118
118
  const helpers = (0, helpers_1.getHelpers)(knex);
119
119
  const relations = schema.relations;
@@ -143,31 +143,34 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
143
143
  followRelation(path);
144
144
  function followRelation(pathParts, parentCollection = collection, parentAlias) {
145
145
  /**
146
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
146
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
147
147
  */
148
148
  const pathRoot = pathParts[0].split(':')[0];
149
- const relation = relations.find((relation) => {
150
- var _a;
151
- return ((relation.collection === parentCollection && relation.field === pathRoot) ||
152
- (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
153
- });
154
- if (!relation)
149
+ const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
150
+ if (!relation) {
155
151
  return;
156
- const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
152
+ }
157
153
  const alias = generateAlias();
158
154
  (0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
159
155
  if (relationType === 'm2o') {
160
156
  dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
161
157
  }
162
- if (relationType === 'm2a') {
158
+ if (relationType === 'a2o') {
163
159
  const pathScope = pathParts[0].split(':')[1];
164
160
  if (!pathScope) {
165
161
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
166
162
  }
167
163
  dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
168
164
  joinClause
169
- .on(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`))
170
- .andOnVal(relation.meta.one_collection_field, '=', pathScope);
165
+ .onVal(relation.meta.one_collection_field, '=', pathScope)
166
+ .andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
167
+ });
168
+ }
169
+ if (relationType === 'o2a') {
170
+ dbQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
171
+ joinClause
172
+ .onVal(relation.meta.one_collection_field, '=', parentCollection)
173
+ .andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
171
174
  });
172
175
  }
173
176
  // Still join o2m relations when in subquery OR when the o2m relation is not at the root level
@@ -179,7 +182,7 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
179
182
  if (relationType === 'm2o') {
180
183
  parent = relation.related_collection;
181
184
  }
182
- else if (relationType === 'm2a') {
185
+ else if (relationType === 'a2o') {
183
186
  const pathScope = pathParts[0].split(':')[1];
184
187
  if (!pathScope) {
185
188
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
@@ -215,17 +218,12 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
215
218
  }
216
219
  const filterPath = getFilterPath(key, value);
217
220
  /**
218
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
221
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
219
222
  */
220
223
  const pathRoot = filterPath[0].split(':')[0];
221
- const relation = relations.find((relation) => {
222
- var _a;
223
- return ((relation.collection === collection && relation.field === pathRoot) ||
224
- (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
225
- });
224
+ const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
226
225
  const { operator: filterOperator, value: filterValue } = getOperation(key, value);
227
- const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection: collection, field: pathRoot }) : null;
228
- if (relationType === 'm2o' || relationType === 'm2a' || relationType === null) {
226
+ if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
229
227
  if (filterPath.length > 1) {
230
228
  const columnName = getWhereColumn(filterPath, collection);
231
229
  if (!columnName)
@@ -237,7 +235,13 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
237
235
  }
238
236
  }
239
237
  else if (subQuery === false) {
240
- const pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
238
+ if (!relation)
239
+ continue;
240
+ let pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
241
+ if (relationType === 'o2a') {
242
+ pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
243
+ }
244
+ // Note: knex's types don't appreciate knex.raw in whereIn, even though it's officially supported
241
245
  dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
242
246
  const field = relation.field;
243
247
  const collection = relation.collection;
@@ -376,22 +380,17 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
376
380
  return followRelation(path);
377
381
  function followRelation(pathParts, parentCollection = collection, parentAlias) {
378
382
  /**
379
- * For M2A fields, the path can contain an optional collection scope <field>:<scope>
383
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
380
384
  */
381
385
  const pathRoot = pathParts[0].split(':')[0];
382
- const relation = relations.find((relation) => {
383
- var _a;
384
- return ((relation.collection === parentCollection && relation.field === pathRoot) ||
385
- (relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
386
- });
386
+ const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
387
387
  if (!relation) {
388
388
  throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
389
389
  }
390
- const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
391
390
  const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
392
391
  const remainingParts = pathParts.slice(1);
393
392
  let parent;
394
- if (relationType === 'm2a') {
393
+ if (relationType === 'a2o') {
395
394
  const pathScope = pathParts[0].split(':')[1];
396
395
  if (!pathScope) {
397
396
  throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
@@ -88,7 +88,7 @@ async function getASTFromQuery(collection, query, schema, options) {
88
88
  const parts = name.split('.');
89
89
  let rootField = parts[0];
90
90
  let collectionScope = null;
91
- // m2a related collection scoped field selector `fields=sections.section_id:headings.title`
91
+ // a2o related collection scoped field selector `fields=sections.section_id:headings.title`
92
92
  if (rootField.includes(':')) {
93
93
  const [key, scope] = rootField.split(':');
94
94
  rootField = key;
@@ -136,14 +136,14 @@ async function getASTFromQuery(collection, query, schema, options) {
136
136
  if (!relationType)
137
137
  continue;
138
138
  let child = null;
139
- if (relationType === 'm2a') {
139
+ if (relationType === 'a2o') {
140
140
  const allowedCollections = relation.meta.one_allowed_collections.filter((collection) => {
141
141
  if (!permissions)
142
142
  return true;
143
143
  return permissions.some((permission) => permission.collection === collection);
144
144
  });
145
145
  child = {
146
- type: 'm2a',
146
+ type: 'a2o',
147
147
  names: allowedCollections,
148
148
  children: {},
149
149
  query: {},
@@ -16,12 +16,14 @@ function getDefaultValue(column) {
16
16
  return null;
17
17
  if (defaultValue === 'NULL')
18
18
  return null;
19
- // Check if the default is wrapped in an extra pair of quotes, this happens in SQLite
19
+ // Check if the default is wrapped in an extra pair of quotes, this happens in SQLite / MariaDB
20
20
  if (typeof defaultValue === 'string' &&
21
21
  ((defaultValue.startsWith(`'`) && defaultValue.endsWith(`'`)) ||
22
22
  (defaultValue.startsWith(`"`) && defaultValue.endsWith(`"`)))) {
23
23
  defaultValue = defaultValue.slice(1, -1);
24
24
  }
25
+ if (defaultValue === '0000-00-00 00:00:00')
26
+ return null;
25
27
  switch (type) {
26
28
  case 'bigInteger':
27
29
  case 'integer':
@@ -0,0 +1,2 @@
1
+ import { Request } from 'express';
2
+ export declare function getIPFromReq(req: Request): string;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getIPFromReq = void 0;
7
+ const net_1 = require("net");
8
+ const env_1 = __importDefault(require("../env"));
9
+ const logger_1 = __importDefault(require("../logger"));
10
+ function getIPFromReq(req) {
11
+ let ip = req.ip;
12
+ if (env_1.default.IP_CUSTOM_HEADER) {
13
+ const customIPHeaderValue = req.get(env_1.default.IP_CUSTOM_HEADER);
14
+ if (typeof customIPHeaderValue === 'string' && (0, net_1.isIP)(customIPHeaderValue) !== 0) {
15
+ ip = customIPHeaderValue;
16
+ }
17
+ else {
18
+ logger_1.default.warn(`Custom IP header didn't return valid IP address: ${JSON.stringify(customIPHeaderValue)}`);
19
+ }
20
+ }
21
+ // IP addresses starting with ::ffff: are IPv4 addresses in IPv6 format. We can strip the prefix to get back to IPv4
22
+ return ip.startsWith('::ffff:') ? ip.substring(7) : ip;
23
+ }
24
+ exports.getIPFromReq = getIPFromReq;
@@ -107,7 +107,7 @@ function getLocalType(column, field) {
107
107
  return 'hash';
108
108
  if (special.includes('csv'))
109
109
  return 'csv';
110
- if (special.includes('uuid'))
110
+ if (special.includes('uuid') || special.includes('file'))
111
111
  return 'uuid';
112
112
  if (type === null || type === void 0 ? void 0 : type.startsWith('geometry')) {
113
113
  return special[0] || 'geometry';
@@ -9,6 +9,7 @@ const lodash_1 = require("lodash");
9
9
  const database_1 = __importDefault(require("../database"));
10
10
  const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
11
11
  const merge_permissions_1 = require("../utils/merge-permissions");
12
+ const merge_permissions_for_share_1 = require("./merge-permissions-for-share");
12
13
  const users_1 = require("../services/users");
13
14
  const roles_1 = require("../services/roles");
14
15
  const cache_1 = require("../cache");
@@ -18,8 +19,8 @@ async function getPermissions(accountability, schema) {
18
19
  const database = (0, database_1.default)();
19
20
  const { systemCache, cache } = (0, cache_1.getCache)();
20
21
  let permissions = [];
21
- const { user, role, app, admin } = accountability;
22
- const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin })}`;
22
+ const { user, role, app, admin, share_scope } = accountability;
23
+ const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin, share_scope })}`;
23
24
  if (env_1.default.CACHE_PERMISSIONS !== false) {
24
25
  const cachedPermissions = await systemCache.get(cacheKey);
25
26
  if (cachedPermissions) {
@@ -44,14 +45,21 @@ async function getPermissions(accountability, schema) {
44
45
  }
45
46
  }
46
47
  if (accountability.admin !== true) {
47
- const permissionsForRole = await database
48
- .select('*')
49
- .from('directus_permissions')
50
- .where({ role: accountability.role });
48
+ const query = database.select('*').from('directus_permissions');
49
+ if (accountability.role) {
50
+ query.where({ role: accountability.role });
51
+ }
52
+ else {
53
+ query.whereNull('role');
54
+ }
55
+ const permissionsForRole = await query;
51
56
  const { permissions: parsedPermissions, requiredPermissionData, containDynamicData, } = parsePermissions(permissionsForRole);
52
57
  permissions = parsedPermissions;
53
58
  if (accountability.app === true) {
54
- permissions = (0, merge_permissions_1.mergePermissions)(permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
59
+ permissions = (0, merge_permissions_1.mergePermissions)('or', permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
60
+ }
61
+ if (accountability.share_scope) {
62
+ permissions = (0, merge_permissions_for_share_1.mergePermissionsForShare)(permissions, accountability, schema);
55
63
  }
56
64
  const filterContext = containDynamicData
57
65
  ? await getFilterContext(schema, accountability, requiredPermissionData)
@@ -3,4 +3,4 @@ export declare function getRelationType(getRelationOptions: {
3
3
  relation: Relation;
4
4
  collection: string | null;
5
5
  field: string;
6
- }): 'm2o' | 'o2m' | 'm2a' | null;
6
+ }): 'm2o' | 'o2m' | 'a2o' | null;
@@ -10,7 +10,7 @@ function getRelationType(getRelationOptions) {
10
10
  relation.field === field &&
11
11
  ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_collection_field) &&
12
12
  ((_b = relation.meta) === null || _b === void 0 ? void 0 : _b.one_allowed_collections)) {
13
- return 'm2a';
13
+ return 'a2o';
14
14
  }
15
15
  if (relation.collection === collection && relation.field === field) {
16
16
  return 'm2o';
@@ -0,0 +1,5 @@
1
+ import { Permission, Accountability, Filter } from '@directus/shared/types';
2
+ import { SchemaOverview } from '../types';
3
+ export declare function mergePermissionsForShare(currentPermissions: Permission[], accountability: Accountability, schema: SchemaOverview): Permission[];
4
+ export declare function traverse(schema: SchemaOverview, rootItemPrimaryKeyField: string, rootItemPrimaryKey: string, currentCollection: string, parentCollections?: string[], path?: string[]): Partial<Permission>[];
5
+ export declare function getFilterForPath(type: 'o2m' | 'm2o' | 'a2o', path: string[], rootPrimaryKeyField: string, rootPrimaryKey: string): Filter;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFilterForPath = exports.traverse = exports.mergePermissionsForShare = void 0;
4
+ const lodash_1 = require("lodash");
5
+ const merge_permissions_1 = require("./merge-permissions");
6
+ const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
7
+ const reduce_schema_1 = require("./reduce-schema");
8
+ function mergePermissionsForShare(currentPermissions, accountability, schema) {
9
+ const defaults = {
10
+ action: 'read',
11
+ role: accountability.role,
12
+ collection: '',
13
+ permissions: {},
14
+ validation: null,
15
+ presets: null,
16
+ fields: null,
17
+ };
18
+ const { collection, item } = accountability.share_scope;
19
+ const parentPrimaryKeyField = schema.collections[collection].primary;
20
+ const reducedSchema = (0, reduce_schema_1.reduceSchema)(schema, currentPermissions, ['read']);
21
+ const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection);
22
+ const parentCollectionPermission = (0, lodash_1.assign)({}, defaults, {
23
+ collection,
24
+ permissions: {
25
+ [parentPrimaryKeyField]: {
26
+ _eq: item,
27
+ },
28
+ },
29
+ });
30
+ // All permissions that will be merged into the original permissions set
31
+ const allGeneratedPermissions = [
32
+ parentCollectionPermission,
33
+ ...relationalPermissions.map((generated) => (0, lodash_1.assign)({}, defaults, generated)),
34
+ ...app_access_permissions_1.schemaPermissions,
35
+ ];
36
+ // All the collections that are touched through the relational tree from the current root collection, and the schema collections
37
+ const allowedCollections = (0, lodash_1.uniq)(allGeneratedPermissions.map(({ collection }) => collection));
38
+ const generatedPermissions = [];
39
+ // Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve)
40
+ // the items of a collection if you entered that collection from multiple angles
41
+ for (const collection of allowedCollections) {
42
+ const permissionsForCollection = allGeneratedPermissions.filter((permission) => permission.collection === collection);
43
+ if (permissionsForCollection.length > 0) {
44
+ generatedPermissions.push(...(0, merge_permissions_1.mergePermissions)('or', permissionsForCollection));
45
+ }
46
+ else {
47
+ generatedPermissions.push(...permissionsForCollection);
48
+ }
49
+ }
50
+ // Explicitly filter out permissions to collections unrelated to the root parent item.
51
+ const limitedPermissions = currentPermissions.filter(({ collection }) => allowedCollections.includes(collection));
52
+ return (0, merge_permissions_1.mergePermissions)('and', limitedPermissions, generatedPermissions);
53
+ }
54
+ exports.mergePermissionsForShare = mergePermissionsForShare;
55
+ function traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, currentCollection, parentCollections = [], path = []) {
56
+ var _a, _b, _c;
57
+ const permissions = [];
58
+ // If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit.
59
+ // This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or
60
+ // articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁
61
+ if (parentCollections.includes(currentCollection)) {
62
+ return permissions;
63
+ }
64
+ const relationsInCollection = schema.relations.filter((relation) => {
65
+ return relation.collection === currentCollection || relation.related_collection === currentCollection;
66
+ });
67
+ for (const relation of relationsInCollection) {
68
+ let type;
69
+ if (relation.related_collection === currentCollection) {
70
+ type = 'o2m';
71
+ }
72
+ else if (!relation.related_collection) {
73
+ type = 'a2o';
74
+ }
75
+ else {
76
+ type = 'm2o';
77
+ }
78
+ if (type === 'o2m') {
79
+ permissions.push({
80
+ collection: relation.collection,
81
+ permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey),
82
+ });
83
+ permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.collection, [...parentCollections, currentCollection], [...path, relation.field]));
84
+ }
85
+ if (type === 'a2o' && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_allowed_collections)) {
86
+ for (const collection of relation.meta.one_allowed_collections) {
87
+ permissions.push({
88
+ collection,
89
+ permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
90
+ });
91
+ }
92
+ }
93
+ if (type === 'm2o') {
94
+ permissions.push({
95
+ collection: relation.related_collection,
96
+ permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
97
+ });
98
+ if ((_b = relation.meta) === null || _b === void 0 ? void 0 : _b.one_field) {
99
+ permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.related_collection, [...parentCollections, currentCollection], [...path, (_c = relation.meta) === null || _c === void 0 ? void 0 : _c.one_field]));
100
+ }
101
+ }
102
+ }
103
+ return permissions;
104
+ }
105
+ exports.traverse = traverse;
106
+ function getFilterForPath(type, path, rootPrimaryKeyField, rootPrimaryKey) {
107
+ const filter = {};
108
+ if (type === 'm2o' || type === 'a2o') {
109
+ (0, lodash_1.set)(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } });
110
+ }
111
+ else {
112
+ (0, lodash_1.set)(filter, path.reverse(), { _eq: rootPrimaryKey });
113
+ }
114
+ return filter;
115
+ }
116
+ exports.getFilterForPath = getFilterForPath;
@@ -1,2 +1,14 @@
1
+ /// <reference types="lodash" />
1
2
  import { Permission } from '@directus/shared/types';
2
- export declare function mergePermissions(...permissions: Permission[][]): Permission[];
3
+ export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): Permission[];
4
+ export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): import("lodash").Omit<{
5
+ permissions: import("@directus/shared/types").Filter | null;
6
+ validation: import("@directus/shared/types").Filter | null;
7
+ fields: string[] | null;
8
+ presets: Record<string, any> | null;
9
+ id?: number | undefined;
10
+ role: string | null;
11
+ collection: string;
12
+ action: import("@directus/shared/types").PermissionsAction;
13
+ system?: true | undefined;
14
+ }, "id" | "system">;