@directus/api 22.0.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/cli/commands/init/questions.d.ts +7 -6
- package/dist/cli/commands/init/questions.js +2 -2
- package/dist/cli/utils/create-env/index.d.ts +2 -2
- package/dist/cli/utils/create-env/index.js +3 -1
- package/dist/cli/utils/drivers.js +1 -1
- 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/helpers/index.d.ts +3 -3
- package/dist/database/index.d.ts +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
- package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
- package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
- package/dist/database/migrations/20240806A-permissions-policies.js +18 -3
- 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/server.js +17 -4
- 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/types/database.d.ts +1 -1
- package/dist/utils/apply-query.d.ts +3 -3
- package/dist/utils/apply-query.js +25 -20
- package/dist/utils/get-address.d.ts +5 -0
- package/dist/utils/get-address.js +13 -0
- 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/dist/utils/transaction.js +28 -11
- package/dist/websocket/controllers/graphql.js +2 -3
- package/dist/websocket/controllers/rest.js +2 -3
- package/package.json +17 -16
|
@@ -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[]>;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { uniq } from 'lodash-es';
|
|
2
2
|
import { fetchPermissions } from '../../lib/fetch-permissions.js';
|
|
3
3
|
import { fetchPolicies } from '../../lib/fetch-policies.js';
|
|
4
|
-
import { withCache } from '../../utils/with-cache.js';
|
|
5
|
-
export const fetchAllowedFields = withCache('allowed-fields', _fetchAllowedFields, ({ action, collection, accountability: { user, role, roles, ip, app } }) => ({
|
|
6
|
-
action,
|
|
7
|
-
collection,
|
|
8
|
-
accountability: { user, role, roles, ip, app },
|
|
9
|
-
}));
|
|
10
4
|
/**
|
|
11
5
|
* Look up all fields that are allowed to be used for the given collection and action for the given
|
|
12
6
|
* accountability object
|
|
@@ -14,7 +8,7 @@ export const fetchAllowedFields = withCache('allowed-fields', _fetchAllowedField
|
|
|
14
8
|
* Done by looking up all available policies for the current accountability object, and reading all
|
|
15
9
|
* permissions that exist for the collection+action+policy combination
|
|
16
10
|
*/
|
|
17
|
-
export async function
|
|
11
|
+
export async function fetchAllowedFields({ accountability, action, collection }, { knex, schema }) {
|
|
18
12
|
const policies = await fetchPolicies(accountability, { knex, schema });
|
|
19
13
|
const permissions = await fetchPermissions({ action, collections: [collection], policies, accountability }, { knex, schema });
|
|
20
14
|
const allowedFields = [];
|
package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts
CHANGED
|
@@ -8,5 +8,4 @@ export interface FetchInconsistentFieldMapOptions {
|
|
|
8
8
|
/**
|
|
9
9
|
* Fetch a field map for fields that may or may not be null based on item-by-item permissions.
|
|
10
10
|
*/
|
|
11
|
-
export declare
|
|
12
|
-
export declare function _fetchInconsistentFieldMap({ accountability, action }: FetchInconsistentFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
|
|
11
|
+
export declare function fetchInconsistentFieldMap({ accountability, action }: FetchInconsistentFieldMapOptions, { knex, schema }: Context): Promise<FieldMap>;
|
package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { uniq, intersection, difference
|
|
1
|
+
import { uniq, intersection, difference } from 'lodash-es';
|
|
2
2
|
import { fetchPolicies } from '../../lib/fetch-policies.js';
|
|
3
|
-
import { withCache } from '../../utils/with-cache.js';
|
|
4
3
|
import { fetchPermissions } from '../../lib/fetch-permissions.js';
|
|
5
4
|
/**
|
|
6
5
|
* Fetch a field map for fields that may or may not be null based on item-by-item permissions.
|
|
7
6
|
*/
|
|
8
|
-
export
|
|
9
|
-
action,
|
|
10
|
-
accountability: accountability ? pick(accountability, ['user', 'role', 'roles', 'ip', 'admin', 'app']) : null,
|
|
11
|
-
}));
|
|
12
|
-
export async function _fetchInconsistentFieldMap({ accountability, action }, { knex, schema }) {
|
|
7
|
+
export async function fetchInconsistentFieldMap({ accountability, action }, { knex, schema }) {
|
|
13
8
|
const fieldMap = {};
|
|
14
9
|
if (!accountability || accountability.admin) {
|
|
15
10
|
for (const collection of Object.keys(schema.collections)) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { dedupeAccess } from '../utils/dedupe-access.js';
|
|
2
|
+
import { hasItemPermissions } from '../utils/has-item-permissions.js';
|
|
3
|
+
export function getCases(collection, permissions, requestedKeys) {
|
|
4
|
+
const permissionsForCollection = permissions.filter((permission) => permission.collection === collection);
|
|
5
|
+
const rules = dedupeAccess(permissionsForCollection);
|
|
6
|
+
const cases = [];
|
|
7
|
+
const caseMap = {};
|
|
8
|
+
// TODO this can be optimized if there is only one rule to skip the whole case/where system,
|
|
9
|
+
// since fields that are not allowed at all are already filtered out
|
|
10
|
+
// TODO this can be optimized if all cases are the same for all requested keys, as those should be
|
|
11
|
+
//
|
|
12
|
+
let index = 0;
|
|
13
|
+
for (const { rule, fields } of rules) {
|
|
14
|
+
// If none of the fields in the current permissions rule overlap with the actually requested
|
|
15
|
+
// fields in the AST, we can ignore this case altogether
|
|
16
|
+
if (requestedKeys.length > 0 &&
|
|
17
|
+
fields.has('*') === false &&
|
|
18
|
+
Array.from(fields).every((field) => requestedKeys.includes(field) === false)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (rule === null)
|
|
22
|
+
continue;
|
|
23
|
+
cases.push(rule);
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
caseMap[field] = [...(caseMap[field] ?? []), index];
|
|
26
|
+
}
|
|
27
|
+
index++;
|
|
28
|
+
}
|
|
29
|
+
// Field that are allowed no matter what conditions exist for the item. These come from
|
|
30
|
+
// permissions where the item read access is "everything"
|
|
31
|
+
const allowedFields = new Set(permissionsForCollection
|
|
32
|
+
.filter((permission) => hasItemPermissions(permission) === false)
|
|
33
|
+
.map((permission) => permission.fields ?? [])
|
|
34
|
+
.flat());
|
|
35
|
+
return {
|
|
36
|
+
cases,
|
|
37
|
+
caseMap,
|
|
38
|
+
allowedFields,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getUnaliasedFieldKey } from '../../../utils/get-unaliased-field-key.js';
|
|
2
|
-
import { dedupeAccess } from '../utils/dedupe-access.js';
|
|
3
|
-
import { hasItemPermissions } from '../utils/has-item-permissions.js';
|
|
4
2
|
import { uniq } from 'lodash-es';
|
|
3
|
+
import { getCases } from './get-cases.js';
|
|
5
4
|
/**
|
|
6
5
|
* Mutates passed AST
|
|
7
6
|
*
|
|
@@ -53,41 +52,3 @@ function processChildren(collection, children, permissions) {
|
|
|
53
52
|
}
|
|
54
53
|
return cases;
|
|
55
54
|
}
|
|
56
|
-
function getCases(collection, permissions, requestedKeys) {
|
|
57
|
-
const permissionsForCollection = permissions.filter((permission) => permission.collection === collection);
|
|
58
|
-
const rules = dedupeAccess(permissionsForCollection);
|
|
59
|
-
const cases = [];
|
|
60
|
-
const caseMap = {};
|
|
61
|
-
// TODO this can be optimized if there is only one rule to skip the whole case/where system,
|
|
62
|
-
// since fields that are not allowed at all are already filtered out
|
|
63
|
-
// TODO this can be optimized if all cases are the same for all requested keys, as those should be
|
|
64
|
-
//
|
|
65
|
-
let index = 0;
|
|
66
|
-
for (const { rule, fields } of rules) {
|
|
67
|
-
// If none of the fields in the current permissions rule overlap with the actually requested
|
|
68
|
-
// fields in the AST, we can ignore this case altogether
|
|
69
|
-
if (requestedKeys.length > 0 &&
|
|
70
|
-
fields.has('*') === false &&
|
|
71
|
-
Array.from(fields).every((field) => requestedKeys.includes(field) === false)) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (rule === null)
|
|
75
|
-
continue;
|
|
76
|
-
cases.push(rule);
|
|
77
|
-
for (const field of fields) {
|
|
78
|
-
caseMap[field] = [...(caseMap[field] ?? []), index];
|
|
79
|
-
}
|
|
80
|
-
index++;
|
|
81
|
-
}
|
|
82
|
-
// Field that are allowed no matter what conditions exist for the item. These come from
|
|
83
|
-
// permissions where the item read access is "everything"
|
|
84
|
-
const allowedFields = new Set(permissionsForCollection
|
|
85
|
-
.filter((permission) => hasItemPermissions(permission) === false)
|
|
86
|
-
.map((permission) => permission.fields ?? [])
|
|
87
|
-
.flat());
|
|
88
|
-
return {
|
|
89
|
-
cases,
|
|
90
|
-
caseMap,
|
|
91
|
-
allowedFields,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
@@ -55,6 +55,8 @@ export async function processPayload(options, context) {
|
|
|
55
55
|
}
|
|
56
56
|
fieldValidationRules.push(field.validation);
|
|
57
57
|
}
|
|
58
|
+
const presets = (permissions ?? []).map((permission) => permission.presets);
|
|
59
|
+
const payloadWithPresets = assign({}, ...presets, options.payload);
|
|
58
60
|
const validationRules = [...fieldValidationRules, ...permissionValidationRules].filter((rule) => {
|
|
59
61
|
if (rule === null)
|
|
60
62
|
return false;
|
|
@@ -64,14 +66,11 @@ export async function processPayload(options, context) {
|
|
|
64
66
|
});
|
|
65
67
|
if (validationRules.length > 0) {
|
|
66
68
|
const validationErrors = [];
|
|
67
|
-
validationErrors.push(...validatePayload({ _and: validationRules },
|
|
69
|
+
validationErrors.push(...validatePayload({ _and: validationRules }, payloadWithPresets)
|
|
68
70
|
.map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
|
|
69
71
|
.flat());
|
|
70
72
|
if (validationErrors.length > 0)
|
|
71
73
|
throw validationErrors;
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
return options.payload;
|
|
75
|
-
const presets = permissions.map((permission) => permission.presets);
|
|
76
|
-
return assign({}, ...presets, options.payload);
|
|
75
|
+
return payloadWithPresets;
|
|
77
76
|
}
|
|
@@ -13,11 +13,6 @@ export async function validateItemAccess(options, context) {
|
|
|
13
13
|
// whole or not
|
|
14
14
|
fields: [],
|
|
15
15
|
limit: options.primaryKeys.length,
|
|
16
|
-
filter: {
|
|
17
|
-
[primaryKeyField]: {
|
|
18
|
-
_in: options.primaryKeys,
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
16
|
};
|
|
22
17
|
const ast = await getAstFromQuery({
|
|
23
18
|
accountability: options.accountability,
|
|
@@ -25,7 +20,13 @@ export async function validateItemAccess(options, context) {
|
|
|
25
20
|
collection: options.collection,
|
|
26
21
|
}, context);
|
|
27
22
|
await processAst({ ast, ...options }, context);
|
|
28
|
-
|
|
23
|
+
// Inject the filter after the permissions have been processed, as to not require access to the primary key
|
|
24
|
+
ast.query.filter = {
|
|
25
|
+
[primaryKeyField]: {
|
|
26
|
+
_in: options.primaryKeys,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const items = await runAst(ast, context.schema, options.accountability, { knex: context.knex });
|
|
29
30
|
if (items && items.length === options.primaryKeys.length) {
|
|
30
31
|
return true;
|
|
31
32
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Accountability, Permission } from '@directus/types';
|
|
2
2
|
import type { Context } from '../types.js';
|
|
3
|
-
export declare const fetchDynamicVariableContext: typeof _fetchDynamicVariableContext;
|
|
4
3
|
export interface FetchDynamicVariableContext {
|
|
5
4
|
accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
|
|
6
5
|
policies: string[];
|
|
7
6
|
permissions: Permission[];
|
|
8
7
|
}
|
|
9
|
-
export declare function
|
|
8
|
+
export declare function fetchDynamicVariableContext(options: FetchDynamicVariableContext, context: Context): Promise<Record<string, any>>;
|
|
@@ -1,43 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
accountability: {
|
|
7
|
-
user,
|
|
8
|
-
role,
|
|
9
|
-
roles,
|
|
10
|
-
},
|
|
11
|
-
}));
|
|
12
|
-
export async function _fetchDynamicVariableContext(options, context) {
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { getSimpleHash } from '@directus/utils';
|
|
3
|
+
import { getCache, getCacheValue, setCacheValue } from '../../cache.js';
|
|
4
|
+
import { extractRequiredDynamicVariableContext, } from './extract-required-dynamic-variable-context.js';
|
|
5
|
+
export async function fetchDynamicVariableContext(options, context) {
|
|
13
6
|
const { UsersService } = await import('../../services/users.js');
|
|
14
7
|
const { RolesService } = await import('../../services/roles.js');
|
|
15
8
|
const { PoliciesService } = await import('../../services/policies.js');
|
|
16
9
|
const contextData = {};
|
|
17
10
|
const permissionContext = extractRequiredDynamicVariableContext(options.permissions);
|
|
18
11
|
if (options.accountability.user && (permissionContext.$CURRENT_USER?.size ?? 0) > 0) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
contextData['$CURRENT_USER'] = await fetchContextData('$CURRENT_USER', permissionContext, { user: options.accountability.user }, async (fields) => {
|
|
13
|
+
const usersService = new UsersService(context);
|
|
14
|
+
return await usersService.readOne(options.accountability.user, {
|
|
15
|
+
fields,
|
|
16
|
+
});
|
|
22
17
|
});
|
|
23
18
|
}
|
|
24
19
|
if (options.accountability.role && (permissionContext.$CURRENT_ROLE?.size ?? 0) > 0) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', permissionContext, { role: options.accountability.role }, async (fields) => {
|
|
21
|
+
const usersService = new RolesService(context);
|
|
22
|
+
return await usersService.readOne(options.accountability.role, {
|
|
23
|
+
fields,
|
|
24
|
+
});
|
|
28
25
|
});
|
|
29
26
|
}
|
|
30
27
|
if (options.accountability.roles.length > 0 && (permissionContext.$CURRENT_ROLES?.size ?? 0) > 0) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
contextData['$CURRENT_ROLES'] = await fetchContextData('$CURRENT_ROLES', permissionContext, { roles: options.accountability.roles }, async (fields) => {
|
|
29
|
+
const rolesService = new RolesService(context);
|
|
30
|
+
return await rolesService.readMany(options.accountability.roles, {
|
|
31
|
+
fields,
|
|
32
|
+
});
|
|
34
33
|
});
|
|
35
34
|
}
|
|
36
35
|
if (options.policies.length > 0 && (permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
|
|
37
|
+
const policiesService = new PoliciesService(context);
|
|
38
|
+
return await policiesService.readMany(options.policies, {
|
|
39
|
+
fields,
|
|
40
|
+
});
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
return contextData;
|
|
43
44
|
}
|
|
45
|
+
async function fetchContextData(key, permissionContext, cacheContext, fetch) {
|
|
46
|
+
const { cache } = getCache();
|
|
47
|
+
const env = useEnv();
|
|
48
|
+
const fields = Array.from(permissionContext[key]);
|
|
49
|
+
const cacheKey = cache
|
|
50
|
+
? `filter-context-${key.slice(1)}-${getSimpleHash(JSON.stringify({ ...cacheContext, fields }))}`
|
|
51
|
+
: '';
|
|
52
|
+
let data = undefined;
|
|
53
|
+
if (cache) {
|
|
54
|
+
data = await getCacheValue(cache, cacheKey);
|
|
55
|
+
}
|
|
56
|
+
if (!data) {
|
|
57
|
+
data = await fetch(fields);
|
|
58
|
+
if (cache && env['CACHE_ENABLED'] !== false) {
|
|
59
|
+
await setCacheValue(cache, cacheKey, data);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Accountability, Permission, PermissionsAction } from '@directus/types';
|
|
2
|
+
import type { Context } from '../types.js';
|
|
3
|
+
export declare const fetchRawPermissions: typeof _fetchRawPermissions;
|
|
4
|
+
export interface FetchRawPermissionsOptions {
|
|
5
|
+
action?: PermissionsAction;
|
|
6
|
+
policies: string[];
|
|
7
|
+
collections?: string[];
|
|
8
|
+
accountability?: Pick<Accountability, 'app'>;
|
|
9
|
+
bypassMinimalAppPermissions?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function _fetchRawPermissions(options: FetchRawPermissionsOptions, context: Context): Promise<Permission[]>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { sortBy } from 'lodash-es';
|
|
2
|
+
import { withAppMinimalPermissions } from '../lib/with-app-minimal-permissions.js';
|
|
3
|
+
import { withCache } from './with-cache.js';
|
|
4
|
+
export const fetchRawPermissions = withCache('raw-permissions', _fetchRawPermissions, ({ action, policies, collections, accountability, bypassMinimalAppPermissions }) => ({
|
|
5
|
+
policies, // we assume that policies always come from the same source, so they should be in the same order
|
|
6
|
+
...(action && { action }),
|
|
7
|
+
...(collections && { collections: sortBy(collections) }),
|
|
8
|
+
...(accountability && { accountability: { app: accountability.app } }),
|
|
9
|
+
...(bypassMinimalAppPermissions && { bypassMinimalAppPermissions }),
|
|
10
|
+
}));
|
|
11
|
+
export async function _fetchRawPermissions(options, context) {
|
|
12
|
+
const { PermissionsService } = await import('../../services/permissions.js');
|
|
13
|
+
const permissionsService = new PermissionsService(context);
|
|
14
|
+
const filter = {
|
|
15
|
+
_and: [{ policy: { _in: options.policies } }],
|
|
16
|
+
};
|
|
17
|
+
if (options.action) {
|
|
18
|
+
filter._and.push({ action: { _eq: options.action } });
|
|
19
|
+
}
|
|
20
|
+
if (options.collections) {
|
|
21
|
+
filter._and.push({ collection: { _in: options.collections } });
|
|
22
|
+
}
|
|
23
|
+
let permissions = (await permissionsService.readByQuery({
|
|
24
|
+
filter,
|
|
25
|
+
limit: -1,
|
|
26
|
+
}));
|
|
27
|
+
// Sort permissions by their order in the policies array
|
|
28
|
+
// This ensures that if a sorted array of policies is passed in the permissions are returned in the same order
|
|
29
|
+
// which is necessary for correctly applying the presets in order
|
|
30
|
+
permissions = sortBy(permissions, (permission) => options.policies.indexOf(permission.policy));
|
|
31
|
+
if (options.accountability && !options.bypassMinimalAppPermissions) {
|
|
32
|
+
// Add app minimal permissions for the request accountability, if applicable.
|
|
33
|
+
// Normally this is done in the permissions service readByQuery, but it also needs to do it here
|
|
34
|
+
// since the permissions service is created without accountability.
|
|
35
|
+
// We call it without the policies filter, since the static minimal app permissions don't have a policy attached.
|
|
36
|
+
return withAppMinimalPermissions(options.accountability ?? null, permissions, { _and: filter._and.slice(1) });
|
|
37
|
+
}
|
|
38
|
+
return permissions;
|
|
39
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import emitter from './emitter.js';
|
|
|
13
13
|
import { useLogger } from './logger/index.js';
|
|
14
14
|
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
15
15
|
import { getIPFromReq } from './utils/get-ip-from-req.js';
|
|
16
|
+
import { getAddress } from './utils/get-address.js';
|
|
16
17
|
import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
|
|
17
18
|
import { startWebSocketHandlers } from './websocket/handlers/index.js';
|
|
18
19
|
export let SERVER_ONLINE = true;
|
|
@@ -116,10 +117,22 @@ export async function createServer() {
|
|
|
116
117
|
export async function startServer() {
|
|
117
118
|
const server = await createServer();
|
|
118
119
|
const host = env['HOST'];
|
|
119
|
-
const
|
|
120
|
+
const path = env['UNIX_SOCKET_PATH'];
|
|
121
|
+
const port = env['PORT'];
|
|
122
|
+
let listenOptions;
|
|
123
|
+
if (path) {
|
|
124
|
+
listenOptions = { path };
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
listenOptions = {
|
|
128
|
+
host,
|
|
129
|
+
port: parseInt(port || '8055'),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
120
132
|
server
|
|
121
|
-
.listen(
|
|
122
|
-
|
|
133
|
+
.listen(listenOptions, () => {
|
|
134
|
+
const protocol = server instanceof https.Server ? 'https' : 'http';
|
|
135
|
+
logger.info(`Server started at ${listenOptions.port ? `${protocol}://${getAddress(server)}` : getAddress(server)}`);
|
|
123
136
|
process.send?.('ready');
|
|
124
137
|
emitter.emitAction('server.start', { server }, {
|
|
125
138
|
database: getDatabase(),
|
|
@@ -129,7 +142,7 @@ export async function startServer() {
|
|
|
129
142
|
})
|
|
130
143
|
.once('error', (err) => {
|
|
131
144
|
if (err?.code === 'EADDRINUSE') {
|
|
132
|
-
logger.error(`Port ${port} is already in use`);
|
|
145
|
+
logger.error(`${listenOptions.port ? `Port ${listenOptions.port}` : getAddress(server)} is already in use`);
|
|
133
146
|
process.exit(1);
|
|
134
147
|
}
|
|
135
148
|
else {
|
|
@@ -30,5 +30,5 @@ export declare class FieldsService {
|
|
|
30
30
|
updateField(collection: string, field: RawField, opts?: MutationOptions): Promise<string>;
|
|
31
31
|
updateFields(collection: string, fields: RawField[], opts?: MutationOptions): Promise<string[]>;
|
|
32
32
|
deleteField(collection: string, field: string, opts?: MutationOptions): Promise<void>;
|
|
33
|
-
addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field,
|
|
33
|
+
addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, existing?: Column | null): void;
|
|
34
34
|
}
|