@directus/api 22.1.0 → 22.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/cache.d.ts +2 -2
  3. package/dist/cache.js +2 -2
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.js +1 -0
  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 +2 -0
  14. package/dist/database/helpers/index.js +2 -0
  15. package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
  16. package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
  17. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
  18. package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
  19. package/dist/database/helpers/nullable-update/index.d.ts +7 -0
  20. package/dist/database/helpers/nullable-update/index.js +7 -0
  21. package/dist/database/helpers/nullable-update/types.d.ts +7 -0
  22. package/dist/database/helpers/nullable-update/types.js +12 -0
  23. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
  24. package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
  25. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  26. package/dist/database/helpers/schema/dialects/mssql.js +20 -0
  27. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  28. package/dist/database/helpers/schema/dialects/mysql.js +33 -0
  29. package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
  30. package/dist/database/helpers/schema/dialects/oracle.js +21 -0
  31. package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
  32. package/dist/database/helpers/schema/dialects/postgres.js +23 -0
  33. package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
  34. package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
  35. package/dist/database/helpers/schema/types.d.ts +5 -0
  36. package/dist/database/helpers/schema/types.js +3 -0
  37. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
  38. package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
  39. package/dist/database/index.d.ts +1 -1
  40. package/dist/database/index.js +2 -2
  41. package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
  42. package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
  43. package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
  44. package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
  45. package/dist/database/run-ast/lib/get-db-query.js +23 -13
  46. package/dist/database/run-ast/run-ast.d.ts +2 -2
  47. package/dist/database/run-ast/run-ast.js +14 -7
  48. package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
  49. package/dist/database/run-ast/utils/apply-case-when.js +2 -2
  50. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
  51. package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
  52. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
  53. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
  54. package/dist/extensions/manager.js +2 -2
  55. package/dist/logger/index.d.ts +6 -0
  56. package/dist/logger/index.js +79 -28
  57. package/dist/logger/logs-stream.d.ts +11 -0
  58. package/dist/logger/logs-stream.js +41 -0
  59. package/dist/middleware/respond.js +1 -0
  60. package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
  61. package/dist/permissions/lib/fetch-permissions.js +5 -39
  62. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
  63. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
  64. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
  65. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
  66. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
  67. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
  68. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
  69. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
  70. package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
  71. package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
  72. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
  73. package/dist/permissions/modules/process-payload/process-payload.js +4 -5
  74. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
  75. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
  76. package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
  77. package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
  78. package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
  79. package/dist/request/is-denied-ip.js +7 -1
  80. package/dist/server.js +4 -2
  81. package/dist/services/fields.d.ts +1 -1
  82. package/dist/services/fields.js +66 -25
  83. package/dist/services/import-export.js +2 -2
  84. package/dist/services/items.js +1 -1
  85. package/dist/services/mail/index.js +1 -5
  86. package/dist/services/meta.js +8 -7
  87. package/dist/services/notifications.d.ts +0 -4
  88. package/dist/services/notifications.js +8 -6
  89. package/dist/services/permissions.js +19 -19
  90. package/dist/services/server.js +8 -1
  91. package/dist/services/specifications.js +7 -7
  92. package/dist/services/users.js +4 -1
  93. package/dist/utils/apply-query.d.ts +3 -3
  94. package/dist/utils/apply-query.js +25 -20
  95. package/dist/utils/get-address.d.ts +1 -1
  96. package/dist/utils/get-address.js +6 -1
  97. package/dist/utils/get-allowed-log-levels.d.ts +3 -0
  98. package/dist/utils/get-allowed-log-levels.js +11 -0
  99. package/dist/utils/get-column.d.ts +8 -4
  100. package/dist/utils/get-column.js +10 -2
  101. package/dist/utils/get-schema.js +19 -24
  102. package/dist/utils/parse-filter-key.js +1 -5
  103. package/dist/utils/sanitize-query.js +1 -1
  104. package/dist/utils/sanitize-schema.d.ts +1 -1
  105. package/dist/websocket/controllers/base.d.ts +10 -10
  106. package/dist/websocket/controllers/base.js +22 -3
  107. package/dist/websocket/controllers/graphql.js +3 -1
  108. package/dist/websocket/controllers/index.d.ts +4 -0
  109. package/dist/websocket/controllers/index.js +12 -0
  110. package/dist/websocket/controllers/logs.d.ts +18 -0
  111. package/dist/websocket/controllers/logs.js +50 -0
  112. package/dist/websocket/controllers/rest.js +3 -1
  113. package/dist/websocket/handlers/index.d.ts +1 -0
  114. package/dist/websocket/handlers/index.js +21 -3
  115. package/dist/websocket/handlers/logs.d.ts +31 -0
  116. package/dist/websocket/handlers/logs.js +121 -0
  117. package/dist/websocket/messages.d.ts +26 -0
  118. package/dist/websocket/messages.js +9 -0
  119. package/dist/websocket/types.d.ts +7 -0
  120. package/package.json +27 -26
@@ -0,0 +1,41 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { Writable } from 'stream';
3
+ import { useBus } from '../bus/index.js';
4
+ const nodeId = nanoid(8);
5
+ export class LogsStream extends Writable {
6
+ messenger;
7
+ pretty;
8
+ constructor(pretty) {
9
+ super({ objectMode: true });
10
+ this.messenger = useBus();
11
+ this.pretty = pretty;
12
+ }
13
+ _write(chunk, _encoding, callback) {
14
+ if (!this.pretty) {
15
+ // keeping this string interpolation for performance on RAW logs
16
+ this.messenger.publish('logs', `{"log":${chunk},"nodeId":"${nodeId}"}`);
17
+ return callback();
18
+ }
19
+ const log = JSON.parse(chunk);
20
+ if (this.pretty === 'http' && log.req?.method && log.req?.url && log.res?.statusCode && log.responseTime) {
21
+ this.messenger.publish('logs', JSON.stringify({
22
+ log: {
23
+ level: log['level'],
24
+ time: log['time'],
25
+ msg: `${log.req.method} ${log.req.url} ${log.res.statusCode} ${log.responseTime}ms`,
26
+ },
27
+ nodeId: nodeId,
28
+ }));
29
+ return callback();
30
+ }
31
+ this.messenger.publish('logs', JSON.stringify({
32
+ log: {
33
+ level: log['level'],
34
+ time: log['time'],
35
+ msg: log['msg'],
36
+ },
37
+ nodeId: nodeId,
38
+ }));
39
+ callback();
40
+ }
41
+ }
@@ -20,6 +20,7 @@ export const respond = asyncHandler(async (req, res) => {
20
20
  exceedsMaxSize = valueSize > maxSize;
21
21
  }
22
22
  if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
23
+ req.originalUrl?.startsWith('/auth') === false &&
23
24
  env['CACHE_ENABLED'] === true &&
24
25
  cache &&
25
26
  !req.sanitizedQuery.export &&
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import os from 'node:os';
3
+ import { matches } from 'ip-matching';
3
4
  import { useLogger } from '../logger/index.js';
4
5
  import { ipInNetworks } from '../utils/ip-in-networks.js';
5
6
  export function isDeniedIp(ip) {
@@ -24,8 +25,13 @@ export function isDeniedIp(ip) {
24
25
  if (!networkInfo)
25
26
  continue;
26
27
  for (const info of networkInfo) {
27
- if (info.address === ip)
28
+ if (info.internal && info.cidr) {
29
+ if (matches(ip, info.cidr))
30
+ return true;
31
+ }
32
+ else if (info.address === ip) {
28
33
  return true;
34
+ }
29
35
  }
30
36
  }
31
37
  }
package/dist/server.js CHANGED
@@ -14,7 +14,7 @@ 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
16
  import { getAddress } from './utils/get-address.js';
17
- import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
17
+ import { createLogsController, createSubscriptionController, createWebSocketController, getLogsController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
18
18
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
19
19
  export let SERVER_ONLINE = true;
20
20
  const env = useEnv();
@@ -77,6 +77,7 @@ export async function createServer() {
77
77
  if (toBoolean(env['WEBSOCKETS_ENABLED']) === true) {
78
78
  createSubscriptionController(server);
79
79
  createWebSocketController(server);
80
+ createLogsController(server);
80
81
  startWebSocketHandlers();
81
82
  }
82
83
  const terminusOptions = {
@@ -99,6 +100,7 @@ export async function createServer() {
99
100
  async function onSignal() {
100
101
  getSubscriptionController()?.terminate();
101
102
  getWebSocketController()?.terminate();
103
+ getLogsController()?.terminate();
102
104
  const database = getDatabase();
103
105
  await database.destroy();
104
106
  logger.info('Database connections destroyed');
@@ -126,7 +128,7 @@ export async function startServer() {
126
128
  else {
127
129
  listenOptions = {
128
130
  host,
129
- port: parseInt(port || '8055'),
131
+ port: parseInt(port),
130
132
  };
131
133
  }
132
134
  server
@@ -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
  }