@directus/api 21.0.0-rc.0 → 22.0.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 (65) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/cache.d.ts +0 -1
  3. package/dist/cache.js +7 -22
  4. package/dist/controllers/tus.js +7 -5
  5. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +1 -1
  6. package/dist/database/get-ast-from-query/lib/parse-fields.js +10 -0
  7. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  8. package/dist/database/helpers/schema/dialects/cockroachdb.js +4 -0
  9. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  10. package/dist/database/helpers/schema/dialects/mssql.js +4 -0
  11. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  12. package/dist/database/helpers/schema/dialects/oracle.js +4 -0
  13. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  14. package/dist/database/helpers/schema/dialects/postgres.js +4 -0
  15. package/dist/database/helpers/schema/types.d.ts +5 -0
  16. package/dist/database/helpers/schema/types.js +3 -0
  17. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +8 -0
  18. package/dist/database/helpers/schema/utils/preprocess-bindings.js +30 -0
  19. package/dist/database/index.js +14 -6
  20. package/dist/database/migrations/20240305A-change-useragent-type.js +1 -1
  21. package/dist/database/migrations/20240716A-update-files-date-fields.js +33 -0
  22. package/dist/database/migrations/20240806A-permissions-policies.d.ts +6 -0
  23. package/dist/database/migrations/20240806A-permissions-policies.js +338 -0
  24. package/dist/database/run-ast/lib/get-db-query.js +12 -2
  25. package/dist/database/run-ast/utils/apply-case-when.js +5 -4
  26. package/dist/database/run-ast/utils/with-preprocess-bindings.d.ts +2 -0
  27. package/dist/database/run-ast/utils/with-preprocess-bindings.js +14 -0
  28. package/dist/logger/index.js +1 -1
  29. package/dist/middleware/error-handler.d.ts +2 -2
  30. package/dist/middleware/error-handler.js +54 -51
  31. package/dist/permissions/lib/fetch-permissions.d.ts +1 -0
  32. package/dist/permissions/lib/fetch-permissions.js +3 -2
  33. package/dist/permissions/lib/fetch-policies.d.ts +7 -0
  34. package/dist/permissions/lib/fetch-policies.js +16 -1
  35. package/dist/permissions/modules/process-ast/lib/inject-cases.js +6 -6
  36. package/dist/permissions/modules/process-ast/types.d.ts +0 -6
  37. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +11 -1
  38. package/dist/permissions/utils/filter-policies-by-ip.d.ts +1 -1
  39. package/dist/services/assets.js +2 -5
  40. package/dist/services/fields.d.ts +3 -0
  41. package/dist/services/fields.js +29 -5
  42. package/dist/services/files/lib/get-sharp-instance.d.ts +2 -0
  43. package/dist/services/files/lib/get-sharp-instance.js +10 -0
  44. package/dist/services/files/utils/get-metadata.js +7 -6
  45. package/dist/services/files.js +5 -0
  46. package/dist/services/import-export.d.ts +3 -1
  47. package/dist/services/import-export.js +49 -5
  48. package/dist/services/mail/index.d.ts +1 -1
  49. package/dist/services/mail/index.js +9 -1
  50. package/dist/services/relations.d.ts +3 -1
  51. package/dist/services/relations.js +27 -5
  52. package/dist/services/tus/data-store.js +4 -5
  53. package/dist/services/tus/server.d.ts +1 -1
  54. package/dist/services/tus/server.js +9 -2
  55. package/dist/utils/apply-query.d.ts +8 -5
  56. package/dist/utils/apply-query.js +40 -5
  57. package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +2 -0
  58. package/dist/utils/fetch-user-count/fetch-access-lookup.js +3 -2
  59. package/dist/utils/fetch-user-count/fetch-user-count.js +10 -3
  60. package/dist/utils/fetch-user-count/get-user-count-query.js +1 -1
  61. package/dist/utils/get-schema.js +3 -3
  62. package/dist/utils/sanitize-schema.d.ts +1 -1
  63. package/package.json +38 -38
  64. package/dist/database/migrations/20240710A-permissions-policies.js +0 -169
  65. /package/dist/database/migrations/{20240710A-permissions-policies.d.ts → 20240716A-update-files-date-fields.d.ts} +0 -0
@@ -4,6 +4,7 @@ import { getFilterOperatorsForType, getFunctionsForType, getOutputTypeForFunctio
4
4
  import { clone, isPlainObject } from 'lodash-es';
5
5
  import { customAlphabet } from 'nanoid/non-secure';
6
6
  import { getHelpers } from '../database/helpers/index.js';
7
+ import { applyCaseWhen } from '../database/run-ast/utils/apply-case-when.js';
7
8
  import { getColumnPath } from './get-column-path.js';
8
9
  import { getColumn } from './get-column.js';
9
10
  import { getRelationInfo } from './get-relation-info.js';
@@ -34,9 +35,6 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
34
35
  if (query.search) {
35
36
  applySearch(knex, schema, dbQuery, query.search, collection);
36
37
  }
37
- if (query.group) {
38
- dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
39
- }
40
38
  // `cases` are the permissions cases that are required for the current data set. We're
41
39
  // dynamically adding those into the filters that the user provided to enforce the permission
42
40
  // rules. You should be able to read an item if one or more of the cases matches. The actual case
@@ -50,6 +48,37 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, cas
50
48
  }
51
49
  hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
52
50
  }
51
+ if (query.group) {
52
+ const rawColumns = query.group.map((column) => getColumn(knex, collection, column, false, schema));
53
+ let columns;
54
+ if (options?.groupWhenCases) {
55
+ columns = rawColumns.map((column, index) => applyCaseWhen({
56
+ columnCases: options.groupWhenCases[index].map((caseIndex) => cases[caseIndex]),
57
+ column,
58
+ aliasMap,
59
+ cases,
60
+ table: collection,
61
+ }, {
62
+ knex,
63
+ schema,
64
+ }));
65
+ if (query.sort && query.sort.length === 1 && query.sort[0] === query.group[0]) {
66
+ // Special case, where the sort query is injected by the group by operation
67
+ dbQuery.clear('order');
68
+ let order = 'asc';
69
+ if (query.sort[0].startsWith('-')) {
70
+ order = 'desc';
71
+ }
72
+ // @ts-expect-error (orderBy does not accept Knex.Raw for some reason, even though it is handled correctly)
73
+ // https://github.com/knex/knex/issues/5711
74
+ dbQuery.orderBy([{ column: columns[0], order }]);
75
+ }
76
+ }
77
+ else {
78
+ columns = rawColumns;
79
+ }
80
+ dbQuery.groupBy(columns);
81
+ }
53
82
  if (query.aggregate) {
54
83
  applyAggregate(schema, dbQuery, query.aggregate, collection, hasJoins);
55
84
  }
@@ -408,11 +437,17 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
408
437
  // Knex supports "raw" in the columnName parameter, but isn't typed as such. Too bad..
409
438
  // See https://github.com/knex/knex/issues/4518 @TODO remove as any once knex is updated
410
439
  // These operators don't rely on a value, and can thus be used without one (eg `?filter[field][_null]`)
411
- if ((operator === '_null' && compareValue !== false) || (operator === '_nnull' && compareValue === false)) {
440
+ if ((operator === '_null' && compareValue !== false) ||
441
+ (operator === '_nnull' && compareValue === false) ||
442
+ (operator === '_eq' && compareValue === null)) {
412
443
  dbQuery[logical].whereNull(selectionRaw);
444
+ return;
413
445
  }
414
- if ((operator === '_nnull' && compareValue !== false) || (operator === '_null' && compareValue === false)) {
446
+ if ((operator === '_nnull' && compareValue !== false) ||
447
+ (operator === '_null' && compareValue === false) ||
448
+ (operator === '_neq' && compareValue === null)) {
415
449
  dbQuery[logical].whereNotNull(selectionRaw);
450
+ return;
416
451
  }
417
452
  if ((operator === '_empty' && compareValue !== false) || (operator === '_nempty' && compareValue === false)) {
418
453
  dbQuery[logical].andWhere((query) => {
@@ -5,6 +5,8 @@ export interface AccessLookup {
5
5
  user: string | null;
6
6
  app_access: boolean | number;
7
7
  admin_access: boolean | number;
8
+ user_status: 'active' | string;
9
+ user_role: string | null;
8
10
  }
9
11
  export interface FetchAccessLookupOptions {
10
12
  excludeAccessRows?: PrimaryKey[];
@@ -1,8 +1,9 @@
1
1
  export async function fetchAccessLookup(options) {
2
2
  let query = options.knex
3
- .select('directus_access.role', 'directus_access.user', 'directus_policies.app_access', 'directus_policies.admin_access')
3
+ .select('directus_access.role', 'directus_access.user', 'directus_policies.app_access', 'directus_policies.admin_access', 'directus_users.status as user_status', 'directus_users.role as user_role')
4
4
  .from('directus_access')
5
- .leftJoin('directus_policies', 'directus_access.policy', 'directus_policies.id');
5
+ .leftJoin('directus_policies', 'directus_access.policy', 'directus_policies.id')
6
+ .leftJoin('directus_users', 'directus_access.user', 'directus_users.id');
6
7
  if (options.excludeAccessRows && options.excludeAccessRows.length > 0) {
7
8
  query = query.whereNotIn('directus_access.id', options.excludeAccessRows);
8
9
  }
@@ -12,7 +12,9 @@ export async function fetchUserCount(options) {
12
12
  .filter((row) => !toBoolean(row.admin_access) && toBoolean(row.app_access) && row.role !== null)
13
13
  .map((row) => row.role));
14
14
  // All users that are directly granted rights through a connected policy
15
- const adminUsers = new Set(accessRows.filter((row) => toBoolean(row.admin_access) && row.user !== null).map((row) => row.user));
15
+ const adminUsers = new Set(accessRows
16
+ .filter((row) => toBoolean(row.admin_access) && row.user !== null && row.user_status === 'active')
17
+ .map((row) => row.user));
16
18
  // Some roles might be granted access rights through nesting, so determine all roles that grant admin or app access,
17
19
  // including nested roles
18
20
  const { adminRoles: allAdminRoles, appRoles: allAppRoles } = await fetchAccessRoles({
@@ -35,7 +37,12 @@ export async function fetchUserCount(options) {
35
37
  };
36
38
  }
37
39
  const appUsers = new Set(accessRows
38
- .filter((row) => !toBoolean(row.admin_access) && toBoolean(row.app_access) && row.user !== null)
40
+ .filter((row) => !toBoolean(row.admin_access) &&
41
+ toBoolean(row.app_access) &&
42
+ row.user !== null &&
43
+ row.user_status === 'active' &&
44
+ adminUsers.has(row.user) === false &&
45
+ adminRoles.has(row.user_role) === false)
39
46
  .map((row) => row.user));
40
47
  // All users that are granted app rights through a role, but not directly, and that aren't admin users
41
48
  const appCountQuery = getUserCountQuery(options.knex, {
@@ -52,6 +59,6 @@ export async function fetchUserCount(options) {
52
59
  return {
53
60
  admin: adminCount,
54
61
  app: appCount,
55
- api: Number(allResult?.['count'] ?? 0) - adminCount - appCount,
62
+ api: Math.max(0, Number(allResult?.['count'] ?? 0) - adminCount - appCount),
56
63
  };
57
64
  }
@@ -3,7 +3,7 @@ export function getUserCountQuery(knex, options) {
3
3
  if (options.includeRoles && options.includeRoles.length === 0) {
4
4
  return Promise.resolve({ count: 0 });
5
5
  }
6
- let query = knex('directus_users').count({ count: '*' }).as('count').where('status', 'active');
6
+ let query = knex('directus_users').count({ count: '*' }).as('count').where('status', '=', 'active');
7
7
  if (options.excludeIds && options.excludeIds.length > 0) {
8
8
  query = query.whereNotIn('id', options.excludeIds);
9
9
  }
@@ -17,9 +17,6 @@ const logger = useLogger();
17
17
  export async function getSchema(options, attempt = 0) {
18
18
  const MAX_ATTEMPTS = 3;
19
19
  const env = useEnv();
20
- if (attempt >= MAX_ATTEMPTS) {
21
- throw new Error(`Failed to get Schema information: hit infinite loop`);
22
- }
23
20
  if (options?.bypassCache || env['CACHE_SCHEMA'] === false) {
24
21
  const database = options?.database || getDatabase();
25
22
  const schemaInspector = createInspector(database);
@@ -29,6 +26,9 @@ export async function getSchema(options, attempt = 0) {
29
26
  if (cached) {
30
27
  return cached;
31
28
  }
29
+ if (attempt >= MAX_ATTEMPTS) {
30
+ throw new Error(`Failed to get Schema information: hit infinite loop`);
31
+ }
32
32
  const lock = useLock();
33
33
  const bus = useBus();
34
34
  const lockKey = 'schemaCache--preparing';
@@ -16,7 +16,7 @@ export declare function sanitizeCollection(collection: Collection | undefined):
16
16
  * @returns sanitized field
17
17
  */
18
18
  export declare function sanitizeField(field: Field | undefined, sanitizeAllSchema?: boolean): Partial<Field> | undefined;
19
- export declare function sanitizeColumn(column: Column): Pick<Column, "name" | "table" | "numeric_precision" | "numeric_scale" | "data_type" | "default_value" | "max_length" | "is_nullable" | "is_unique" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
19
+ export declare function sanitizeColumn(column: Column): Pick<Column, "table" | "name" | "numeric_precision" | "numeric_scale" | "data_type" | "default_value" | "max_length" | "is_nullable" | "is_unique" | "is_primary_key" | "is_generated" | "generation_expression" | "has_auto_increment" | "foreign_key_table" | "foreign_key_column">;
20
20
  /**
21
21
  * Pick certain database vendor specific relation properties that should be compared when performing diff
22
22
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "21.0.0-rc.0",
3
+ "version": "22.0.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "dependencies": {
61
61
  "@authenio/samlify-node-xmllint": "2.0.0",
62
- "@aws-sdk/client-ses": "3.600.0",
62
+ "@aws-sdk/client-ses": "3.614.0",
63
63
  "@godaddy/terminus": "4.12.1",
64
64
  "@rollup/plugin-alias": "5.1.0",
65
65
  "@rollup/plugin-node-resolve": "15.2.3",
@@ -70,7 +70,7 @@
70
70
  "@types/cookie": "0.6.0",
71
71
  "argon2": "0.40.3",
72
72
  "async": "3.2.5",
73
- "axios": "1.7.2",
73
+ "axios": "1.7.3",
74
74
  "busboy": "1.6.0",
75
75
  "bytes": "3.1.2",
76
76
  "camelcase": "8.0.0",
@@ -99,7 +99,7 @@
99
99
  "graphql-ws": "5.16.0",
100
100
  "helmet": "7.1.0",
101
101
  "icc": "3.0.0",
102
- "inquirer": "9.3.2",
102
+ "inquirer": "9.3.5",
103
103
  "ioredis": "5.4.1",
104
104
  "ip-matching": "2.1.2",
105
105
  "isolated-vm": "4.7.2",
@@ -111,12 +111,12 @@
111
111
  "keyv": "4.5.4",
112
112
  "knex": "3.1.0",
113
113
  "ldapjs": "2.3.3",
114
- "liquidjs": "10.14.0",
114
+ "liquidjs": "10.15.0",
115
115
  "lodash-es": "4.17.21",
116
116
  "marked": "12.0.2",
117
117
  "micromustache": "8.0.3",
118
118
  "mime-types": "2.1.35",
119
- "minimatch": "9.0.4",
119
+ "minimatch": "9.0.5",
120
120
  "mnemonist": "0.39.8",
121
121
  "ms": "2.1.3",
122
122
  "nanoid": "5.0.7",
@@ -124,7 +124,7 @@
124
124
  "node-schedule": "2.1.1",
125
125
  "nodemailer": "6.9.14",
126
126
  "object-hash": "3.0.0",
127
- "openapi3-ts": "4.3.2",
127
+ "openapi3-ts": "4.3.3",
128
128
  "openid-client": "5.6.5",
129
129
  "ora": "8.0.1",
130
130
  "otplib": "12.0.1",
@@ -135,7 +135,7 @@
135
135
  "pino-http": "9.0.0",
136
136
  "pino-http-print": "3.1.0",
137
137
  "pino-pretty": "11.2.1",
138
- "qs": "6.12.2",
138
+ "qs": "6.12.3",
139
139
  "rate-limiter-flexible": "5.0.3",
140
140
  "rollup": "4.17.2",
141
141
  "samlify": "2.8.10",
@@ -143,35 +143,35 @@
143
143
  "sharp": "0.33.4",
144
144
  "snappy": "7.2.2",
145
145
  "stream-json": "1.8.0",
146
- "tar": "7.4.0",
147
- "tsx": "4.12.0",
146
+ "tar": "7.4.2",
147
+ "tsx": "4.16.5",
148
148
  "wellknown": "0.5.0",
149
149
  "ws": "8.18.0",
150
150
  "zod": "3.23.8",
151
151
  "zod-validation-error": "3.3.0",
152
- "@directus/constants": "11.1.0-rc.1",
153
- "@directus/env": "1.3.1-rc.0",
154
- "@directus/errors": "0.4.0-rc.1",
155
- "@directus/app": "13.0.0-rc.2",
156
- "@directus/extensions": "2.0.0-rc.1",
157
- "@directus/extensions-registry": "1.0.10-rc.0",
158
- "@directus/extensions-sdk": "11.0.10-rc.0",
159
- "@directus/memory": "1.1.0-rc.1",
160
- "@directus/format-title": "10.1.2",
161
- "@directus/pressure": "1.0.22-rc.0",
162
- "@directus/schema": "11.0.3",
163
- "@directus/specs": "10.2.10",
164
- "@directus/storage": "10.1.0",
165
- "@directus/storage-driver-azure": "10.0.24-rc.0",
166
- "@directus/storage-driver-cloudinary": "10.0.24-rc.0",
167
- "@directus/storage-driver-gcs": "10.0.25-rc.0",
168
- "@directus/storage-driver-local": "10.1.0",
169
- "@directus/storage-driver-s3": "10.1.1-rc.0",
170
- "@directus/storage-driver-supabase": "1.0.16-rc.0",
171
- "@directus/validation": "0.0.19-rc.0",
172
- "directus": "11.0.0-rc.3",
173
- "@directus/system-data": "2.0.0-rc.1",
174
- "@directus/utils": "12.0.0-rc.1"
152
+ "@directus/constants": "12.0.0",
153
+ "@directus/app": "13.0.0",
154
+ "@directus/env": "2.0.0",
155
+ "@directus/errors": "1.0.0",
156
+ "@directus/format-title": "11.0.0",
157
+ "@directus/extensions": "2.0.0",
158
+ "@directus/extensions-sdk": "12.0.0",
159
+ "@directus/extensions-registry": "2.0.0",
160
+ "@directus/pressure": "2.0.0",
161
+ "@directus/memory": "2.0.0",
162
+ "@directus/schema": "12.0.0",
163
+ "@directus/specs": "11.0.0",
164
+ "@directus/storage": "11.0.0",
165
+ "@directus/storage-driver-azure": "11.0.0",
166
+ "@directus/storage-driver-gcs": "11.0.0",
167
+ "@directus/storage-driver-local": "11.0.0",
168
+ "@directus/storage-driver-cloudinary": "11.0.0",
169
+ "@directus/storage-driver-s3": "11.0.0",
170
+ "@directus/storage-driver-supabase": "2.0.0",
171
+ "@directus/system-data": "2.0.0",
172
+ "@directus/validation": "1.0.0",
173
+ "@directus/utils": "12.0.0",
174
+ "directus": "11.0.0"
175
175
  },
176
176
  "devDependencies": {
177
177
  "@ngneat/falso": "7.2.0",
@@ -196,7 +196,7 @@
196
196
  "@types/lodash-es": "4.17.12",
197
197
  "@types/mime-types": "2.1.4",
198
198
  "@types/ms": "0.7.34",
199
- "@types/node": "18.19.33",
199
+ "@types/node": "18.19.43",
200
200
  "@types/node-schedule": "2.1.7",
201
201
  "@types/nodemailer": "6.4.15",
202
202
  "@types/object-hash": "3.0.6",
@@ -205,16 +205,16 @@
205
205
  "@types/sanitize-html": "2.11.0",
206
206
  "@types/stream-json": "1.7.7",
207
207
  "@types/wellknown": "0.5.8",
208
- "@types/ws": "8.5.10",
208
+ "@types/ws": "8.5.11",
209
209
  "@vitest/coverage-v8": "1.5.3",
210
210
  "copyfiles": "2.4.1",
211
211
  "form-data": "4.0.0",
212
212
  "knex-mock-client": "2.0.1",
213
213
  "typescript": "5.4.5",
214
214
  "vitest": "1.5.3",
215
- "@directus/random": "0.2.8",
216
- "@directus/tsconfig": "1.0.1",
217
- "@directus/types": "12.0.0-rc.1"
215
+ "@directus/random": "1.0.0",
216
+ "@directus/tsconfig": "2.0.0",
217
+ "@directus/types": "12.0.0"
218
218
  },
219
219
  "optionalDependencies": {
220
220
  "@keyv/redis": "2.8.5",
@@ -1,169 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { processChunk } from '@directus/utils';
3
- /**
4
- * The public role used to be `null`, we gotta create a single new policy for the permissions
5
- * previously attached to the public role (marked through `role = null`).
6
- */
7
- const PUBLIC_POLICY_ID = 'abf8a154-5b1c-4a46-ac9c-7300570f4f17';
8
- export async function up(knex) {
9
- /////////////////////////////////////////////////////////////////////////////////////////////////
10
- // If the policies table already exists the migration has already run
11
- if (await knex.schema.hasTable('directus_policies')) {
12
- return;
13
- }
14
- /////////////////////////////////////////////////////////////////////////////////////////////////
15
- // Create new policies table that mirrors previous Roles
16
- await knex.schema.createTable('directus_policies', (table) => {
17
- table.uuid('id').primary();
18
- table.string('name', 100).notNullable();
19
- table.string('icon', 64).notNullable().defaultTo('badge');
20
- table.text('description');
21
- table.text('ip_access');
22
- table.boolean('enforce_tfa').defaultTo(false).notNullable();
23
- table.boolean('admin_access').defaultTo(false).notNullable();
24
- table.boolean('app_access').defaultTo(false).notNullable();
25
- });
26
- /////////////////////////////////////////////////////////////////////////////////////////////////
27
- // Copy over all existing roles into new policies
28
- const roles = await knex
29
- .select('id', 'name', 'icon', 'description', 'ip_access', 'enforce_tfa', 'admin_access', 'app_access')
30
- .from('directus_roles');
31
- if (roles.length > 0) {
32
- await processChunk(roles, 100, async (chunk) => {
33
- await knex('directus_policies').insert(chunk);
34
- });
35
- }
36
- await knex
37
- .insert({
38
- id: PUBLIC_POLICY_ID,
39
- name: '$t:public_label',
40
- icon: 'public',
41
- description: '$t:public_description',
42
- app_access: false,
43
- })
44
- .into('directus_policies');
45
- // Change the admin policy description to $t:admin_policy_description
46
- await knex('directus_policies')
47
- .update({
48
- description: '$t:admin_policy_description',
49
- })
50
- .where('description', 'LIKE', '$t:admin_description');
51
- /////////////////////////////////////////////////////////////////////////////////////////////////
52
- // Remove access control + add nesting to roles
53
- await knex.schema.alterTable('directus_roles', (table) => {
54
- table.dropColumn('ip_access');
55
- table.dropColumn('enforce_tfa');
56
- table.dropColumn('admin_access');
57
- table.dropColumn('app_access');
58
- table.uuid('parent').references('directus_roles.id');
59
- });
60
- /////////////////////////////////////////////////////////////////////////////////////////////////
61
- // Link permissions to policies instead of roles
62
- await knex.schema.alterTable('directus_permissions', (table) => {
63
- table.uuid('policy').references('directus_policies.id').onDelete('CASCADE');
64
- // Drop the foreign key constraint here in order to update `null` role to public policy ID
65
- table.dropForeign('role');
66
- });
67
- await knex('directus_permissions')
68
- .update({
69
- role: PUBLIC_POLICY_ID,
70
- })
71
- .whereNull('role');
72
- await knex('directus_permissions').update({
73
- policy: knex.ref('role'),
74
- });
75
- await knex.schema.alterTable('directus_permissions', (table) => {
76
- table.dropColumns('role');
77
- table.dropNullable('policy');
78
- });
79
- /////////////////////////////////////////////////////////////////////////////////////////////////
80
- // Setup junction table between roles/users and policies
81
- // This could be a A2O style setup with a collection/item field rather than individual foreign
82
- // keys, but we want to be able to show the reverse-relationship on the individual policies as
83
- // well, which would require the O2A type to exist in Directus which currently doesn't.
84
- // Shouldn't be the end of the world here, as we know we're only attaching policies to two other
85
- // collections.
86
- await knex.schema.createTable('directus_access', (table) => {
87
- table.uuid('id').primary();
88
- table.uuid('role').references('directus_roles.id').nullable().onDelete('CASCADE');
89
- table.uuid('user').references('directus_users.id').nullable().onDelete('CASCADE');
90
- table.uuid('policy').references('directus_policies.id').notNullable().onDelete('CASCADE');
91
- table.integer('sort');
92
- });
93
- /////////////////////////////////////////////////////////////////////////////////////////////////
94
- // Attach policies to existing roles for backwards compatibility
95
- const policyAttachments = roles.map((role) => ({
96
- id: randomUUID(),
97
- role: role.id,
98
- user: null,
99
- policy: role.id,
100
- sort: 1,
101
- }));
102
- await processChunk(policyAttachments, 100, async (chunk) => {
103
- await knex('directus_access').insert(chunk);
104
- });
105
- await knex('directus_access').insert({
106
- id: randomUUID(),
107
- role: null,
108
- user: null,
109
- policy: PUBLIC_POLICY_ID,
110
- sort: 1,
111
- });
112
- }
113
- export async function down(knex) {
114
- /////////////////////////////////////////////////////////////////////////////////////////////////
115
- // Reinstate access control fields on directus roles + remove nesting
116
- await knex.schema.alterTable('directus_roles', (table) => {
117
- table.text('ip_access');
118
- table.boolean('enforce_tfa').defaultTo(false).notNullable();
119
- table.boolean('admin_access').defaultTo(false).notNullable();
120
- table.boolean('app_access').defaultTo(true).notNullable();
121
- table.dropForeign('parent');
122
- table.dropColumn('parent');
123
- });
124
- /////////////////////////////////////////////////////////////////////////////////////////////////
125
- // Copy policy access control rules back to roles
126
- const policies = await knex
127
- .select('id', 'ip_access', 'enforce_tfa', 'admin_access', 'app_access')
128
- .from('directus_policies')
129
- .whereNot({ id: PUBLIC_POLICY_ID });
130
- for (const policy of policies) {
131
- await knex('directus_roles')
132
- .update({
133
- ip_access: policy.ip_access,
134
- enforce_tfa: policy.enforce_tfa,
135
- admin_access: policy.admin_access,
136
- app_access: policy.app_access,
137
- })
138
- .where({ id: policy.id });
139
- }
140
- /////////////////////////////////////////////////////////////////////////////////////////////////
141
- // Drop all permissions that are only attached to a user
142
- // TODO query all policies that are attached to a user and delete their permissions,
143
- // since we don't know were to put them now and it'll cause a foreign key problem
144
- // as soon as we reference directus_roles in directus_permissions again
145
- /////////////////////////////////////////////////////////////////////////////////////////////////
146
- // Drop policy attachments
147
- await knex.schema.dropTable('directus_access');
148
- /////////////////////////////////////////////////////////////////////////////////////////////////
149
- // Reattach permissions to roles instead of policies
150
- await knex.schema.alterTable('directus_permissions', (table) => {
151
- table.uuid('role').nullable();
152
- });
153
- await knex('directus_permissions').update({
154
- role: knex.ref('policy'),
155
- });
156
- await knex('directus_permissions')
157
- .update({
158
- role: null,
159
- })
160
- .where({ role: PUBLIC_POLICY_ID });
161
- await knex.schema.alterTable('directus_permissions', (table) => {
162
- table.uuid('role').references('directus_roles.id').alter();
163
- table.dropForeign('policy');
164
- table.dropColumn('policy');
165
- });
166
- /////////////////////////////////////////////////////////////////////////////////////////////////
167
- // Drop policies table
168
- await knex.schema.dropTable('directus_policies');
169
- }