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