@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.
- package/dist/database/get-ast-from-query/get-ast-from-query.js +2 -31
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +21 -3
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.d.ts +9 -0
- package/dist/database/get-ast-from-query/utils/get-allowed-sort.js +35 -0
- package/dist/database/helpers/fn/types.d.ts +6 -3
- package/dist/database/helpers/fn/types.js +2 -2
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20240806A-permissions-policies.js +3 -2
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +9 -5
- package/dist/database/run-ast/run-ast.d.ts +2 -2
- package/dist/database/run-ast/run-ast.js +14 -7
- package/dist/database/run-ast/utils/apply-case-when.d.ts +3 -2
- package/dist/database/run-ast/utils/apply-case-when.js +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-column-pre-processor.js +3 -1
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +2 -2
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +2 -1
- package/dist/permissions/lib/fetch-permissions.d.ts +2 -3
- package/dist/permissions/lib/fetch-permissions.js +5 -39
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -13
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -6
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +1 -2
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +1 -7
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +1 -2
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -7
- package/dist/permissions/modules/process-ast/lib/get-cases.d.ts +6 -0
- package/dist/permissions/modules/process-ast/lib/get-cases.js +40 -0
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -40
- package/dist/permissions/modules/process-payload/process-payload.js +4 -5
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +7 -6
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +44 -24
- package/dist/permissions/utils/fetch-raw-permissions.d.ts +11 -0
- package/dist/permissions/utils/fetch-raw-permissions.js +39 -0
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +22 -19
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/meta.js +8 -7
- package/dist/services/permissions.js +19 -19
- package/dist/utils/apply-query.d.ts +3 -3
- package/dist/utils/apply-query.js +25 -20
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/sanitize-query.js +1 -1
- package/package.json +15 -14
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Generate an AST based on a given collection and query
|
|
3
3
|
*/
|
|
4
4
|
import { cloneDeep, uniq } from 'lodash-es';
|
|
5
|
-
import { fetchAllowedFields } from '../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
6
5
|
import { parseFields } from './lib/parse-fields.js';
|
|
6
|
+
import { getAllowedSort } from './utils/get-allowed-sort.js';
|
|
7
7
|
export async function getAstFromQuery(options, context) {
|
|
8
8
|
options.query = cloneDeep(options.query);
|
|
9
9
|
const ast = {
|
|
@@ -36,36 +36,7 @@ export async function getAstFromQuery(options, context) {
|
|
|
36
36
|
// Prevent fields/deep from showing up in the query object in further use
|
|
37
37
|
delete options.query.fields;
|
|
38
38
|
delete options.query.deep;
|
|
39
|
-
|
|
40
|
-
// We'll default to the primary key for the standard sort output
|
|
41
|
-
let sortField = context.schema.collections[options.collection].primary;
|
|
42
|
-
// If a custom manual sort field is configured, use that
|
|
43
|
-
if (context.schema.collections[options.collection]?.sortField) {
|
|
44
|
-
sortField = context.schema.collections[options.collection].sortField;
|
|
45
|
-
}
|
|
46
|
-
if (options.accountability && options.accountability.admin === false) {
|
|
47
|
-
// Verify that the user has access to the sort field
|
|
48
|
-
const allowedFields = await fetchAllowedFields({
|
|
49
|
-
collection: options.collection,
|
|
50
|
-
action: 'read',
|
|
51
|
-
accountability: options.accountability,
|
|
52
|
-
}, context);
|
|
53
|
-
if (allowedFields.length === 0) {
|
|
54
|
-
sortField = null;
|
|
55
|
-
}
|
|
56
|
-
else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
|
|
57
|
-
// If the sort field is not allowed, default to the first allowed field
|
|
58
|
-
sortField = allowedFields[0];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// When group by is used, default to the first column provided in the group by clause
|
|
62
|
-
if (options.query.group?.[0]) {
|
|
63
|
-
sortField = options.query.group[0];
|
|
64
|
-
}
|
|
65
|
-
if (sortField) {
|
|
66
|
-
options.query.sort = [sortField];
|
|
67
|
-
}
|
|
68
|
-
}
|
|
39
|
+
options.query.sort ??= await getAllowedSort(options, context);
|
|
69
40
|
// When no group by is supplied, but an aggregate function is used, only a single row will be
|
|
70
41
|
// returned. In those cases, we'll ignore the sort field altogether
|
|
71
42
|
if (options.query.aggregate && Object.keys(options.query.aggregate).length && !options.query.group?.[0]) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
|
-
import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../../../types/index.js';
|
|
3
|
+
import type { FieldNode, FunctionFieldNode, NestedCollectionNode, O2MNode } from '../../../types/index.js';
|
|
4
4
|
export interface ParseFieldsOptions {
|
|
5
5
|
accountability: Accountability | null;
|
|
6
6
|
parentCollection: string;
|
|
@@ -13,3 +13,4 @@ export interface ParseFieldsContext {
|
|
|
13
13
|
knex: Knex;
|
|
14
14
|
}
|
|
15
15
|
export declare function parseFields(options: ParseFieldsOptions, context: ParseFieldsContext): Promise<[] | (NestedCollectionNode | FieldNode | FunctionFieldNode)[]>;
|
|
16
|
+
export declare function isO2MNode(node: NestedCollectionNode | null): node is O2MNode;
|
|
@@ -3,6 +3,7 @@ import { isEmpty } from 'lodash-es';
|
|
|
3
3
|
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
4
4
|
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
5
5
|
import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
6
|
+
import { getAllowedSort } from '../utils/get-allowed-sort.js';
|
|
6
7
|
import { getDeepQuery } from '../utils/get-deep-query.js';
|
|
7
8
|
import { getRelatedCollection } from '../utils/get-related-collection.js';
|
|
8
9
|
import { getRelation } from '../utils/get-relation.js';
|
|
@@ -119,7 +120,16 @@ export async function parseFields(options, context) {
|
|
|
119
120
|
continue;
|
|
120
121
|
let child = null;
|
|
121
122
|
if (relationType === 'a2o') {
|
|
122
|
-
|
|
123
|
+
let allowedCollections = relation.meta.one_allowed_collections;
|
|
124
|
+
if (options.accountability && options.accountability.admin === false && policies) {
|
|
125
|
+
const permissions = await fetchPermissions({
|
|
126
|
+
action: 'read',
|
|
127
|
+
collections: allowedCollections,
|
|
128
|
+
policies: policies,
|
|
129
|
+
accountability: options.accountability,
|
|
130
|
+
}, context);
|
|
131
|
+
allowedCollections = allowedCollections.filter((collection) => permissions.some((permission) => permission.collection === collection));
|
|
132
|
+
}
|
|
123
133
|
child = {
|
|
124
134
|
type: 'a2o',
|
|
125
135
|
names: allowedCollections,
|
|
@@ -181,8 +191,13 @@ export async function parseFields(options, context) {
|
|
|
181
191
|
cases: [],
|
|
182
192
|
whenCase: [],
|
|
183
193
|
};
|
|
184
|
-
if (
|
|
185
|
-
child.query.sort =
|
|
194
|
+
if (isO2MNode(child) && !child.query.sort) {
|
|
195
|
+
child.query.sort = await getAllowedSort({ collection: relation.collection, relation, accountability: options.accountability }, context);
|
|
196
|
+
}
|
|
197
|
+
if (isO2MNode(child) && child.query.group && child.query.group[0] !== relation.field) {
|
|
198
|
+
// If a group by is used, the result needs to be grouped by the foreign key of the relation first, so results
|
|
199
|
+
// are correctly grouped under the foreign key when extracting the grouped results from the nested queries.
|
|
200
|
+
child.query.group.unshift(relation.field);
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
if (child) {
|
|
@@ -198,3 +213,6 @@ export async function parseFields(options, context) {
|
|
|
198
213
|
return true;
|
|
199
214
|
});
|
|
200
215
|
}
|
|
216
|
+
export function isO2MNode(node) {
|
|
217
|
+
return !!node && node.type === 'o2m';
|
|
218
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Accountability, Query, Relation } from '@directus/types';
|
|
2
|
+
import type { Context } from '../../../permissions/types.js';
|
|
3
|
+
export type GetAllowedSortFieldOptions = {
|
|
4
|
+
collection: string;
|
|
5
|
+
accountability: Accountability | null;
|
|
6
|
+
query?: Query;
|
|
7
|
+
relation?: Relation;
|
|
8
|
+
};
|
|
9
|
+
export declare function getAllowedSort(options: GetAllowedSortFieldOptions, context: Context): Promise<string[] | null>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fetchAllowedFields } from '../../../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
2
|
+
export async function getAllowedSort(options, context) {
|
|
3
|
+
// We'll default to the primary key for the standard sort output
|
|
4
|
+
let sortField = context.schema.collections[options.collection].primary;
|
|
5
|
+
// If a custom manual sort field is configured, use that
|
|
6
|
+
if (context.schema.collections[options.collection]?.sortField) {
|
|
7
|
+
sortField = context.schema.collections[options.collection].sortField;
|
|
8
|
+
}
|
|
9
|
+
// If a sort field is defined on the relation, use that
|
|
10
|
+
if (options.relation?.meta?.sort_field) {
|
|
11
|
+
sortField = options.relation.meta.sort_field;
|
|
12
|
+
}
|
|
13
|
+
if (options.accountability && options.accountability.admin === false) {
|
|
14
|
+
// Verify that the user has access to the sort field
|
|
15
|
+
const allowedFields = await fetchAllowedFields({
|
|
16
|
+
collection: options.collection,
|
|
17
|
+
action: 'read',
|
|
18
|
+
accountability: options.accountability,
|
|
19
|
+
}, context);
|
|
20
|
+
if (allowedFields.length === 0) {
|
|
21
|
+
sortField = null;
|
|
22
|
+
}
|
|
23
|
+
else if (allowedFields.includes('*') === false && allowedFields.includes(sortField) === false) {
|
|
24
|
+
// If the sort field is not allowed, default to the first allowed field
|
|
25
|
+
sortField = allowedFields[0];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// When group by is used, default to the first column provided in the group by clause
|
|
29
|
+
if (options.query?.group?.[0]) {
|
|
30
|
+
sortField = options.query.group[0];
|
|
31
|
+
}
|
|
32
|
+
if (sortField)
|
|
33
|
+
return [sortField];
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { Filter, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import { DatabaseHelper } from '../types.js';
|
|
4
4
|
export type FnHelperOptions = {
|
|
5
5
|
type: string | undefined;
|
|
6
|
-
query: Query | undefined;
|
|
7
|
-
cases: Filter[] | undefined;
|
|
8
6
|
originalCollectionName: string | undefined;
|
|
7
|
+
relationalCountOptions: {
|
|
8
|
+
query: Query;
|
|
9
|
+
cases: Filter[];
|
|
10
|
+
permissions: Permission[];
|
|
11
|
+
} | undefined;
|
|
9
12
|
};
|
|
10
13
|
export declare abstract class FnHelper extends DatabaseHelper {
|
|
11
14
|
protected schema: SchemaOverview;
|
|
@@ -20,7 +20,7 @@ export class FnHelper extends DatabaseHelper {
|
|
|
20
20
|
.count('*')
|
|
21
21
|
.from({ [alias]: relation.collection })
|
|
22
22
|
.where(this.knex.raw(`??.??`, [alias, relation.field]), '=', this.knex.raw(`??.??`, [table, currentPrimary]));
|
|
23
|
-
if (options?.query
|
|
23
|
+
if (options?.relationalCountOptions?.query.filter) {
|
|
24
24
|
// set the newly aliased collection in the alias map as the default parent collection, indicated by '', for any nested filters
|
|
25
25
|
const aliasMap = {
|
|
26
26
|
'': {
|
|
@@ -28,7 +28,7 @@ export class FnHelper extends DatabaseHelper {
|
|
|
28
28
|
collection: relation.collection,
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
|
-
countQuery = applyFilter(this.knex, this.schema, countQuery, options.query.filter, relation.collection, aliasMap, options.cases
|
|
31
|
+
countQuery = applyFilter(this.knex, this.schema, countQuery, options.relationalCountOptions.query.filter, relation.collection, aliasMap, options.relationalCountOptions.cases, options.relationalCountOptions.permissions).query;
|
|
32
32
|
}
|
|
33
33
|
return this.knex.raw('(' + countQuery.toQuery() + ')');
|
|
34
34
|
}
|
package/dist/database/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Knex } from 'knex';
|
|
|
3
3
|
import type { DatabaseClient } from '../types/index.js';
|
|
4
4
|
export default getDatabase;
|
|
5
5
|
export declare function getDatabase(): Knex;
|
|
6
|
-
export declare function getSchemaInspector(): SchemaInspector;
|
|
6
|
+
export declare function getSchemaInspector(database?: Knex): SchemaInspector;
|
|
7
7
|
/**
|
|
8
8
|
* Get database version. Value currently exists for MySQL only.
|
|
9
9
|
*
|
package/dist/database/index.js
CHANGED
|
@@ -143,11 +143,11 @@ export function getDatabase() {
|
|
|
143
143
|
});
|
|
144
144
|
return database;
|
|
145
145
|
}
|
|
146
|
-
export function getSchemaInspector() {
|
|
146
|
+
export function getSchemaInspector(database) {
|
|
147
147
|
if (inspector) {
|
|
148
148
|
return inspector;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
database ??= getDatabase();
|
|
151
151
|
inspector = createInspector(database);
|
|
152
152
|
return inspector;
|
|
153
153
|
}
|
|
@@ -186,10 +186,10 @@ export async function up(knex) {
|
|
|
186
186
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
187
187
|
// Link permissions to policies instead of roles
|
|
188
188
|
await knex.schema.alterTable('directus_permissions', (table) => {
|
|
189
|
-
table.uuid('policy')
|
|
189
|
+
table.uuid('policy');
|
|
190
190
|
});
|
|
191
191
|
try {
|
|
192
|
-
const inspector = await getSchemaInspector();
|
|
192
|
+
const inspector = await getSchemaInspector(knex);
|
|
193
193
|
const foreignKeys = await inspector.foreignKeys('directus_permissions');
|
|
194
194
|
const foreignConstraint = foreignKeys.find((foreign) => foreign.foreign_key_table === 'directus_roles' && foreign.column === 'role')
|
|
195
195
|
?.constraint_name || undefined;
|
|
@@ -212,6 +212,7 @@ export async function up(knex) {
|
|
|
212
212
|
await knex.schema.alterTable('directus_permissions', (table) => {
|
|
213
213
|
table.dropColumns('role');
|
|
214
214
|
table.dropNullable('policy');
|
|
215
|
+
table.foreign('policy').references('directus_policies.id').onDelete('CASCADE');
|
|
215
216
|
});
|
|
216
217
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
217
218
|
// Setup junction table between roles/users and policies
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
|
|
4
|
-
export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[]): Knex.QueryBuilder;
|
|
4
|
+
export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[], permissions: Permission[]): Knex.QueryBuilder;
|
|
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
|
|
|
9
9
|
import { getNodeAlias } from '../utils/get-field-alias.js';
|
|
10
10
|
import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
|
|
11
11
|
import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
|
|
12
|
-
export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases) {
|
|
12
|
+
export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases, permissions) {
|
|
13
13
|
const aliasMap = Object.create(null);
|
|
14
14
|
const env = useEnv();
|
|
15
|
-
const preProcess = getColumnPreprocessor(knex, schema, table, cases, aliasMap);
|
|
15
|
+
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
|
|
16
16
|
const queryCopy = cloneDeep(query);
|
|
17
17
|
const helpers = getHelpers(knex);
|
|
18
18
|
const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
|
|
@@ -25,7 +25,10 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
25
25
|
const groupWhenCases = hasCaseWhen
|
|
26
26
|
? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
|
|
27
27
|
: undefined;
|
|
28
|
-
const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases,
|
|
28
|
+
const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, permissions, {
|
|
29
|
+
aliasMap,
|
|
30
|
+
groupWhenCases,
|
|
31
|
+
}).query;
|
|
29
32
|
flatQuery.select(fieldNodes.map((node) => preProcess(node)));
|
|
30
33
|
withPreprocessBindings(knex, dbQuery);
|
|
31
34
|
return dbQuery;
|
|
@@ -42,7 +45,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
42
45
|
hasMultiRelationalSort = sortResult.hasMultiRelationalSort;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
|
-
const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, {
|
|
48
|
+
const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, permissions, {
|
|
46
49
|
aliasMap,
|
|
47
50
|
isInnerQuery: true,
|
|
48
51
|
hasMultiRelationalSort,
|
|
@@ -68,6 +71,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
68
71
|
cases,
|
|
69
72
|
table,
|
|
70
73
|
alias: node.fieldKey,
|
|
74
|
+
permissions,
|
|
71
75
|
}, { knex, schema });
|
|
72
76
|
}));
|
|
73
77
|
}
|
|
@@ -159,7 +163,7 @@ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cas
|
|
|
159
163
|
SELECT ...,
|
|
160
164
|
CASE WHEN `inner`.<random-prefix>_<alias> > 0 THEN <actual-column> END AS <alias>
|
|
161
165
|
*/
|
|
162
|
-
const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, innerCaseWhenAliasPrefix);
|
|
166
|
+
const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, innerCaseWhenAliasPrefix);
|
|
163
167
|
// To optimize the query we avoid having unnecessary columns in the inner query, that don't have a caseWhen, since
|
|
164
168
|
// they are selected in the outer query directly
|
|
165
169
|
dbQuery.select(fieldNodes.map(innerPreprocess).filter((x) => x !== null));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Item, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Accountability, Item, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { AST, NestedCollectionNode } from '../../types/ast.js';
|
|
3
3
|
import type { RunASTOptions } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Execute a given AST using Knex. Returns array of items based on requested AST.
|
|
6
6
|
*/
|
|
7
|
-
export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, options?: RunASTOptions): Promise<null | Item | Item[]>;
|
|
7
|
+
export declare function runAst(originalAST: AST | NestedCollectionNode, schema: SchemaOverview, accountability: Accountability | null, options?: RunASTOptions): Promise<null | Item | Item[]>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { cloneDeep, merge } from 'lodash-es';
|
|
3
|
+
import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
|
|
4
|
+
import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
|
|
3
5
|
import { PayloadService } from '../../services/payload.js';
|
|
4
6
|
import getDatabase from '../index.js';
|
|
5
7
|
import { getDBQuery } from './lib/get-db-query.js';
|
|
@@ -10,26 +12,31 @@ import { removeTemporaryFields } from './utils/remove-temporary-fields.js';
|
|
|
10
12
|
/**
|
|
11
13
|
* Execute a given AST using Knex. Returns array of items based on requested AST.
|
|
12
14
|
*/
|
|
13
|
-
export async function runAst(originalAST, schema, options) {
|
|
15
|
+
export async function runAst(originalAST, schema, accountability, options) {
|
|
14
16
|
const ast = cloneDeep(originalAST);
|
|
15
17
|
const knex = options?.knex || getDatabase();
|
|
16
18
|
if (ast.type === 'a2o') {
|
|
17
19
|
const results = {};
|
|
18
20
|
for (const collection of ast.names) {
|
|
19
|
-
results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? []);
|
|
21
|
+
results[collection] = await run(collection, ast.children[collection], ast.query[collection], ast.cases[collection] ?? [], accountability);
|
|
20
22
|
}
|
|
21
23
|
return results;
|
|
22
24
|
}
|
|
23
25
|
else {
|
|
24
|
-
return await run(ast.name, ast.children, options?.query || ast.query, ast.cases);
|
|
26
|
+
return await run(ast.name, ast.children, options?.query || ast.query, ast.cases, accountability);
|
|
25
27
|
}
|
|
26
|
-
async function run(collection, children, query, cases) {
|
|
28
|
+
async function run(collection, children, query, cases, accountability) {
|
|
27
29
|
const env = useEnv();
|
|
28
30
|
// Retrieve the database columns to select in the current AST
|
|
29
31
|
const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(schema, collection, children, query);
|
|
30
32
|
const o2mNodes = nestedCollectionNodes.filter((node) => node.type === 'o2m');
|
|
33
|
+
let permissions = [];
|
|
34
|
+
if (accountability && !accountability.admin) {
|
|
35
|
+
const policies = await fetchPolicies(accountability, { schema, knex });
|
|
36
|
+
permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
|
|
37
|
+
}
|
|
31
38
|
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
|
32
|
-
const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases);
|
|
39
|
+
const dbQuery = getDBQuery(schema, knex, collection, fieldNodes, o2mNodes, query, cases, permissions);
|
|
33
40
|
const rawItems = await dbQuery;
|
|
34
41
|
if (!rawItems)
|
|
35
42
|
return null;
|
|
@@ -72,7 +79,7 @@ export async function runAst(originalAST, schema, options) {
|
|
|
72
79
|
page: null,
|
|
73
80
|
},
|
|
74
81
|
});
|
|
75
|
-
nestedItems = (await runAst(node, schema, { knex, nested: true }));
|
|
82
|
+
nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
|
|
76
83
|
if (nestedItems) {
|
|
77
84
|
items = mergeWithParentItems(schema, nestedItems, items, nestedNode, fieldAllowed);
|
|
78
85
|
}
|
|
@@ -86,7 +93,7 @@ export async function runAst(originalAST, schema, options) {
|
|
|
86
93
|
const node = merge({}, nestedNode, {
|
|
87
94
|
query: { limit: -1 },
|
|
88
95
|
});
|
|
89
|
-
nestedItems = (await runAst(node, schema, { knex, nested: true }));
|
|
96
|
+
nestedItems = (await runAst(node, schema, accountability, { knex, nested: true }));
|
|
90
97
|
if (nestedItems) {
|
|
91
98
|
// Merge all fetched nested records with the parent items
|
|
92
99
|
items = mergeWithParentItems(schema, nestedItems, items, nestedNode, true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { AliasMap } from '../../../utils/get-column-path.js';
|
|
4
4
|
export interface ApplyCaseWhenOptions {
|
|
@@ -8,9 +8,10 @@ export interface ApplyCaseWhenOptions {
|
|
|
8
8
|
cases: Filter[];
|
|
9
9
|
aliasMap: AliasMap;
|
|
10
10
|
alias?: string;
|
|
11
|
+
permissions: Permission[];
|
|
11
12
|
}
|
|
12
13
|
export interface ApplyCaseWhenContext {
|
|
13
14
|
knex: Knex;
|
|
14
15
|
schema: SchemaOverview;
|
|
15
16
|
}
|
|
16
|
-
export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
|
|
17
|
+
export declare function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }: ApplyCaseWhenOptions, { knex, schema }: ApplyCaseWhenContext): Knex.Raw;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { applyFilter } from '../../../utils/apply-query.js';
|
|
2
|
-
export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias }, { knex, schema }) {
|
|
2
|
+
export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, alias, permissions }, { knex, schema }) {
|
|
3
3
|
const caseQuery = knex.queryBuilder();
|
|
4
|
-
applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases);
|
|
4
|
+
applyFilter(knex, schema, caseQuery, { _or: columnCases }, table, aliasMap, cases, permissions);
|
|
5
5
|
const compiler = knex.client.queryCompiler(caseQuery);
|
|
6
6
|
const sqlParts = [];
|
|
7
7
|
// Only empty filters, so no where was generated, skip it
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Filter, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { FieldNode, FunctionFieldNode, M2ONode } from '../../../types/ast.js';
|
|
4
4
|
import type { AliasMap } from '../../../utils/get-column-path.js';
|
|
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
|
|
|
6
6
|
/** Don't assign an alias to the column but instead return the column as is */
|
|
7
7
|
noAlias?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
9
|
+
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
10
10
|
export {};
|
|
@@ -4,7 +4,7 @@ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
|
|
|
4
4
|
import { getHelpers } from '../../helpers/index.js';
|
|
5
5
|
import { applyCaseWhen } from './apply-case-when.js';
|
|
6
6
|
import { getNodeAlias } from './get-field-alias.js';
|
|
7
|
-
export function getColumnPreprocessor(knex, schema, table, cases, aliasMap) {
|
|
7
|
+
export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
|
|
8
8
|
const helpers = getHelpers(knex);
|
|
9
9
|
return function (fieldNode, options) {
|
|
10
10
|
// Don't assign an alias to the column expression if the field has a whenCase
|
|
@@ -32,6 +32,7 @@ export function getColumnPreprocessor(knex, schema, table, cases, aliasMap) {
|
|
|
32
32
|
...fieldNode.query,
|
|
33
33
|
filter: joinFilterWithCases(fieldNode.query.filter, fieldNode.cases),
|
|
34
34
|
},
|
|
35
|
+
permissions,
|
|
35
36
|
cases: fieldNode.cases,
|
|
36
37
|
});
|
|
37
38
|
}
|
|
@@ -50,6 +51,7 @@ export function getColumnPreprocessor(knex, schema, table, cases, aliasMap) {
|
|
|
50
51
|
cases,
|
|
51
52
|
table,
|
|
52
53
|
alias,
|
|
54
|
+
permissions,
|
|
53
55
|
}, { knex, schema });
|
|
54
56
|
}
|
|
55
57
|
return column;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Filter, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { Filter, Permission, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { FieldNode, FunctionFieldNode, M2ONode, O2MNode } from '../../../types/index.js';
|
|
4
4
|
import type { AliasMap } from '../../../utils/get-column-path.js';
|
|
5
|
-
export declare function getInnerQueryColumnPreProcessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], aliasMap: AliasMap, aliasPrefix: string): (fieldNode: FieldNode | FunctionFieldNode | M2ONode | O2MNode) => Knex.Raw<string> | null;
|
|
5
|
+
export declare function getInnerQueryColumnPreProcessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, aliasPrefix: string): (fieldNode: FieldNode | FunctionFieldNode | M2ONode | O2MNode) => Knex.Raw<string> | null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyCaseWhen } from './apply-case-when.js';
|
|
2
2
|
import { getNodeAlias } from './get-field-alias.js';
|
|
3
|
-
export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, aliasPrefix) {
|
|
3
|
+
export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, permissions, aliasMap, aliasPrefix) {
|
|
4
4
|
return function (fieldNode) {
|
|
5
5
|
const alias = getNodeAlias(fieldNode);
|
|
6
6
|
if (fieldNode.whenCase && fieldNode.whenCase.length > 0) {
|
|
@@ -15,6 +15,7 @@ export function getInnerQueryColumnPreProcessor(knex, schema, table, cases, alia
|
|
|
15
15
|
aliasMap,
|
|
16
16
|
cases,
|
|
17
17
|
table,
|
|
18
|
+
permissions,
|
|
18
19
|
}, { knex, schema });
|
|
19
20
|
return knex.raw('COUNT(??) AS ??', [caseWhen, `${aliasPrefix}_${alias}`]);
|
|
20
21
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Accountability,
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
10
|
+
permissions,
|
|
45
11
|
}, context);
|
|
46
12
|
// Replace dynamic variables with their actual values
|
|
47
13
|
const processedPermissions = processPermissions({
|
|
48
|
-
permissions
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15
|
+
export declare function fetchAllowedFields({ accountability, action, collection }: FetchAllowedFieldsOptions, { knex, schema }: Context): Promise<string[]>;
|