@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.
- package/dist/app.js +1 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +7 -22
- package/dist/controllers/tus.js +7 -5
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +1 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +10 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +4 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +4 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +4 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +4 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +8 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +30 -0
- package/dist/database/index.js +14 -6
- package/dist/database/migrations/20240305A-change-useragent-type.js +1 -1
- package/dist/database/migrations/20240716A-update-files-date-fields.js +33 -0
- package/dist/database/migrations/20240806A-permissions-policies.d.ts +6 -0
- package/dist/database/migrations/20240806A-permissions-policies.js +338 -0
- package/dist/database/run-ast/lib/get-db-query.js +12 -2
- package/dist/database/run-ast/utils/apply-case-when.js +5 -4
- package/dist/database/run-ast/utils/with-preprocess-bindings.d.ts +2 -0
- package/dist/database/run-ast/utils/with-preprocess-bindings.js +14 -0
- package/dist/logger/index.js +1 -1
- package/dist/middleware/error-handler.d.ts +2 -2
- package/dist/middleware/error-handler.js +54 -51
- package/dist/permissions/lib/fetch-permissions.d.ts +1 -0
- package/dist/permissions/lib/fetch-permissions.js +3 -2
- package/dist/permissions/lib/fetch-policies.d.ts +7 -0
- package/dist/permissions/lib/fetch-policies.js +16 -1
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +6 -6
- package/dist/permissions/modules/process-ast/types.d.ts +0 -6
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +11 -1
- package/dist/permissions/utils/filter-policies-by-ip.d.ts +1 -1
- package/dist/services/assets.js +2 -5
- package/dist/services/fields.d.ts +3 -0
- package/dist/services/fields.js +29 -5
- package/dist/services/files/lib/get-sharp-instance.d.ts +2 -0
- package/dist/services/files/lib/get-sharp-instance.js +10 -0
- package/dist/services/files/utils/get-metadata.js +7 -6
- package/dist/services/files.js +5 -0
- package/dist/services/import-export.d.ts +3 -1
- package/dist/services/import-export.js +49 -5
- package/dist/services/mail/index.d.ts +1 -1
- package/dist/services/mail/index.js +9 -1
- package/dist/services/relations.d.ts +3 -1
- package/dist/services/relations.js +27 -5
- package/dist/services/tus/data-store.js +4 -5
- package/dist/services/tus/server.d.ts +1 -1
- package/dist/services/tus/server.js +9 -2
- package/dist/utils/apply-query.d.ts +8 -5
- package/dist/utils/apply-query.js +40 -5
- package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +2 -0
- package/dist/utils/fetch-user-count/fetch-access-lookup.js +3 -2
- package/dist/utils/fetch-user-count/fetch-user-count.js +10 -3
- package/dist/utils/fetch-user-count/get-user-count-query.js +1 -1
- package/dist/utils/get-schema.js +3 -3
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/package.json +38 -38
- package/dist/database/migrations/20240710A-permissions-policies.js +0 -169
- /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) ||
|
|
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) ||
|
|
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
|
|
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) &&
|
|
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
|
}
|
package/dist/utils/get-schema.js
CHANGED
|
@@ -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, "
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
147
|
-
"tsx": "4.
|
|
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": "
|
|
153
|
-
"@directus/
|
|
154
|
-
"@directus/
|
|
155
|
-
"@directus/
|
|
156
|
-
"@directus/
|
|
157
|
-
"@directus/extensions
|
|
158
|
-
"@directus/extensions-sdk": "
|
|
159
|
-
"@directus/
|
|
160
|
-
"@directus/
|
|
161
|
-
"@directus/
|
|
162
|
-
"@directus/schema": "
|
|
163
|
-
"@directus/specs": "
|
|
164
|
-
"@directus/storage": "
|
|
165
|
-
"@directus/storage-driver-azure": "
|
|
166
|
-
"@directus/storage-driver-
|
|
167
|
-
"@directus/storage-driver-
|
|
168
|
-
"@directus/storage-driver-
|
|
169
|
-
"@directus/storage-driver-s3": "
|
|
170
|
-
"@directus/storage-driver-supabase": "
|
|
171
|
-
"@directus/
|
|
172
|
-
"directus": "
|
|
173
|
-
"@directus/
|
|
174
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
216
|
-
"@directus/tsconfig": "
|
|
217
|
-
"@directus/types": "12.0.0
|
|
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
|
-
}
|
|
File without changes
|