@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
@@ -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
+ }
@@ -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
  }
@@ -621,7 +621,7 @@ export class FieldsService {
621
621
  }
622
622
  }
623
623
  }
624
- addColumnToTable(table, field, alter = null) {
624
+ addColumnToTable(table, field, existing = null) {
625
625
  let column;
626
626
  // Don't attempt to add a DB column for alias / corrupt fields
627
627
  if (field.type === 'alias' || field.type === 'unknown')
@@ -662,45 +662,48 @@ export class FieldsService {
662
662
  else {
663
663
  throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
664
664
  }
665
- if (field.schema?.default_value !== undefined) {
666
- if (typeof field.schema.default_value === 'string' &&
667
- (field.schema.default_value.toLowerCase() === 'now()' || field.schema.default_value === 'CURRENT_TIMESTAMP')) {
665
+ const defaultValue = field.schema?.default_value !== undefined ? field.schema?.default_value : existing?.default_value;
666
+ if (defaultValue) {
667
+ const newDefaultValueIsString = typeof defaultValue === 'string';
668
+ const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
669
+ const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
670
+ const newDefaultIsSetToCurrentTime = newDefaultIsNowFunction || newDefaultIsCurrentTimestamp;
671
+ const newDefaultIsTimestampWithPrecision = newDefaultValueIsString && defaultValue.includes('CURRENT_TIMESTAMP(') && defaultValue.includes(')');
672
+ if (newDefaultIsSetToCurrentTime) {
668
673
  column.defaultTo(this.knex.fn.now());
669
674
  }
670
- else if (typeof field.schema.default_value === 'string' &&
671
- field.schema.default_value.includes('CURRENT_TIMESTAMP(') &&
672
- field.schema.default_value.includes(')')) {
673
- const precision = field.schema.default_value.match(REGEX_BETWEEN_PARENS)[1];
675
+ else if (newDefaultIsTimestampWithPrecision) {
676
+ const precision = defaultValue.match(REGEX_BETWEEN_PARENS)[1];
674
677
  column.defaultTo(this.knex.fn.now(Number(precision)));
675
678
  }
676
679
  else {
677
- column.defaultTo(field.schema.default_value);
680
+ column.defaultTo(defaultValue);
678
681
  }
679
682
  }
680
- if (field.schema?.is_nullable === false) {
681
- if (!alter || alter.is_nullable === true) {
682
- column.notNullable();
683
- }
683
+ else {
684
+ column.defaultTo(null);
685
+ }
686
+ const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
687
+ if (isNullable) {
688
+ column.nullable();
684
689
  }
685
690
  else {
686
- if (!alter || alter.is_nullable === false) {
687
- column.nullable();
688
- }
691
+ column.notNullable();
689
692
  }
690
693
  if (field.schema?.is_primary_key) {
691
694
  column.primary().notNullable();
692
695
  }
693
696
  else if (field.schema?.is_unique === true) {
694
- if (!alter || alter.is_unique === false) {
697
+ if (!existing || existing.is_unique === false) {
695
698
  column.unique();
696
699
  }
697
700
  }
698
701
  else if (field.schema?.is_unique === false) {
699
- if (alter && alter.is_unique === true) {
702
+ if (existing && existing.is_unique === true) {
700
703
  table.dropUnique([field.field]);
701
704
  }
702
705
  }
703
- if (alter) {
706
+ if (existing) {
704
707
  column.alter();
705
708
  }
706
709
  }
@@ -133,6 +133,8 @@ export class ImportService {
133
133
  };
134
134
  const PapaOptions = {
135
135
  header: true,
136
+ // Trim whitespaces in headers, including the byte order mark (BOM) zero-width no-break space
137
+ transformHeader: (header) => header.trim(),
136
138
  transform,
137
139
  };
138
140
  return new Promise((resolve, reject) => {
@@ -304,7 +306,6 @@ export class ExportService {
304
306
  const savedFile = await filesService.uploadOne(createReadStream(tmpFile.path), fileWithDefaults);
305
307
  if (this.accountability?.user) {
306
308
  const notificationsService = new NotificationsService({
307
- accountability: this.accountability,
308
309
  schema: this.schema,
309
310
  });
310
311
  const usersService = new UsersService({
@@ -333,7 +334,6 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
333
334
  logger.error(err, `Couldn't export ${collection}: ${err.message}`);
334
335
  if (this.accountability?.user) {
335
336
  const notificationsService = new NotificationsService({
336
- accountability: this.accountability,
337
337
  schema: this.schema,
338
338
  });
339
339
  await notificationsService.createOne({
@@ -370,7 +370,7 @@ export class ItemsService {
370
370
  knex: this.knex,
371
371
  });
372
372
  ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
373
- const records = await runAst(ast, this.schema, {
373
+ const records = await runAst(ast, this.schema, this.accountability, {
374
374
  knex: this.knex,
375
375
  // GraphQL requires relational keys to be returned regardless
376
376
  stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
@@ -45,14 +45,14 @@ export class MetaService {
45
45
  action: 'read',
46
46
  policies,
47
47
  accountability: this.accountability,
48
- ...(collection ? { collections: [collection] } : {}),
49
48
  }, context);
50
- const rules = dedupeAccess(permissions);
49
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
50
+ const rules = dedupeAccess(collectionPermissions);
51
51
  const cases = rules.map(({ rule }) => rule);
52
52
  const filter = {
53
53
  _or: cases,
54
54
  };
55
- const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases);
55
+ const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions);
56
56
  hasJoins = result.hasJoins;
57
57
  }
58
58
  if (hasJoins) {
@@ -70,6 +70,7 @@ export class MetaService {
70
70
  let filter = query.filter || {};
71
71
  let hasJoins = false;
72
72
  let cases = [];
73
+ let permissions = [];
73
74
  if (this.accountability && this.accountability.admin === false) {
74
75
  const context = { knex: this.knex, schema: this.schema };
75
76
  await validateAccess({
@@ -78,13 +79,13 @@ export class MetaService {
78
79
  collection,
79
80
  }, context);
80
81
  const policies = await fetchPolicies(this.accountability, context);
81
- const permissions = await fetchPermissions({
82
+ permissions = await fetchPermissions({
82
83
  action: 'read',
83
84
  policies,
84
85
  accountability: this.accountability,
85
- ...(collection ? { collections: [collection] } : {}),
86
86
  }, context);
87
- const rules = dedupeAccess(permissions);
87
+ const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
88
+ const rules = dedupeAccess(collectionPermissions);
88
89
  cases = rules.map(({ rule }) => rule);
89
90
  const permissionsFilter = {
90
91
  _or: cases,
@@ -97,7 +98,7 @@ export class MetaService {
97
98
  }
98
99
  }
99
100
  if (Object.keys(filter).length > 0) {
100
- ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases));
101
+ ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions));
101
102
  }
102
103
  if (query.search) {
103
104
  applySearch(this.knex, this.schema, dbQuery, query.search, collection);
@@ -1,5 +1,8 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
+ import { uniq } from 'lodash-es';
2
3
  import { clearSystemCache } from '../cache.js';
4
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
5
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
3
6
  import { withAppMinimalPermissions } from '../permissions/lib/with-app-minimal-permissions.js';
4
7
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
5
8
  import { ItemsService } from './items.js';
@@ -105,27 +108,24 @@ export class PermissionsService extends ItemsService {
105
108
  .catch(() => { });
106
109
  }));
107
110
  if (schema?.singleton && itemPermissions.update.access) {
108
- const query = {
109
- filter: {
110
- _and: [
111
- ...(this.accountability?.role ? [{ role: { _eq: this.accountability.role } }] : []),
112
- { collection: { _eq: collection } },
113
- { action: { _eq: updateAction } },
114
- ],
115
- },
116
- fields: ['presets', 'fields'],
117
- };
118
- try {
119
- const result = await this.readByQuery(query);
120
- const permission = result[0];
121
- if (permission) {
122
- itemPermissions.update.presets = permission['presets'];
123
- itemPermissions.update.fields = permission['fields'];
111
+ const context = { schema: this.schema, knex: this.knex };
112
+ const policies = await fetchPolicies(this.accountability, context);
113
+ const permissions = await fetchPermissions({ policies, accountability: this.accountability, action: updateAction, collections: [collection] }, context);
114
+ let fields = [];
115
+ let presets = {};
116
+ for (const permission of permissions) {
117
+ if (permission.fields && fields[0] !== '*') {
118
+ fields = uniq([...fields, ...permission.fields]);
119
+ if (fields.includes('*')) {
120
+ fields = ['*'];
121
+ }
122
+ }
123
+ if (permission.presets) {
124
+ presets = { ...(presets ?? {}), ...permission.presets };
124
125
  }
125
126
  }
126
- catch {
127
- // No permission
128
- }
127
+ itemPermissions.update.fields = fields;
128
+ itemPermissions.update.presets = presets;
129
129
  }
130
130
  return itemPermissions;
131
131
  }
@@ -1,4 +1,4 @@
1
- import type { Aggregate, Filter, Query, SchemaOverview } from '@directus/types';
1
+ import type { Aggregate, Filter, Permission, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { AliasMap } from './get-column-path.js';
4
4
  export declare const generateAlias: (size?: number | undefined) => string;
@@ -11,7 +11,7 @@ type ApplyQueryOptions = {
11
11
  /**
12
12
  * Apply the Query to a given Knex query builder instance
13
13
  */
14
- export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], options?: ApplyQueryOptions): {
14
+ export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], permissions: Permission[], options?: ApplyQueryOptions): {
15
15
  query: Knex.QueryBuilder<any, any>;
16
16
  hasJoins: boolean;
17
17
  hasMultiRelationalFilter: boolean;
@@ -34,7 +34,7 @@ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery:
34
34
  };
35
35
  export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
36
36
  export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
37
- export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[]): {
37
+ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[], permissions: Permission[]): {
38
38
  query: Knex.QueryBuilder<any, any>;
39
39
  hasJoins: boolean;
40
40
  hasMultiRelationalFilter: boolean;