@directus/api 22.1.0 → 22.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +2 -2
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- 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 +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
- package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
- package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
- package/dist/database/helpers/nullable-update/index.d.ts +7 -0
- package/dist/database/helpers/nullable-update/index.js +7 -0
- package/dist/database/helpers/nullable-update/types.d.ts +7 -0
- package/dist/database/helpers/nullable-update/types.js +12 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +20 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +33 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/oracle.js +21 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
- package/dist/database/helpers/schema/dialects/postgres.js +23 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
- package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
- 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/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
- package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +2 -2
- package/dist/database/run-ast/lib/get-db-query.js +23 -13
- 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/extensions/manager.js +2 -2
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +79 -28
- package/dist/logger/logs-stream.d.ts +11 -0
- package/dist/logger/logs-stream.js +41 -0
- package/dist/middleware/respond.js +1 -0
- 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/request/is-denied-ip.js +7 -1
- package/dist/server.js +4 -2
- package/dist/services/fields.d.ts +1 -1
- package/dist/services/fields.js +66 -25
- package/dist/services/import-export.js +2 -2
- package/dist/services/items.js +1 -1
- package/dist/services/mail/index.js +1 -5
- package/dist/services/meta.js +8 -7
- package/dist/services/notifications.d.ts +0 -4
- package/dist/services/notifications.js +8 -6
- package/dist/services/permissions.js +19 -19
- package/dist/services/server.js +8 -1
- package/dist/services/specifications.js +7 -7
- package/dist/services/users.js +4 -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 +1 -1
- package/dist/utils/get-address.js +6 -1
- package/dist/utils/get-allowed-log-levels.d.ts +3 -0
- package/dist/utils/get-allowed-log-levels.js +11 -0
- package/dist/utils/get-column.d.ts +8 -4
- package/dist/utils/get-column.js +10 -2
- package/dist/utils/get-schema.js +19 -24
- package/dist/utils/parse-filter-key.js +1 -5
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/controllers/base.d.ts +10 -10
- package/dist/websocket/controllers/base.js +22 -3
- package/dist/websocket/controllers/graphql.js +3 -1
- package/dist/websocket/controllers/index.d.ts +4 -0
- package/dist/websocket/controllers/index.js +12 -0
- package/dist/websocket/controllers/logs.d.ts +18 -0
- package/dist/websocket/controllers/logs.js +50 -0
- package/dist/websocket/controllers/rest.js +3 -1
- package/dist/websocket/handlers/index.d.ts +1 -0
- package/dist/websocket/handlers/index.js +21 -3
- package/dist/websocket/handlers/logs.d.ts +31 -0
- package/dist/websocket/handlers/logs.js +121 -0
- package/dist/websocket/messages.d.ts +26 -0
- package/dist/websocket/messages.js +9 -0
- package/dist/websocket/types.d.ts +7 -0
- package/package.json +27 -26
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { Writable } from 'stream';
|
|
3
|
+
import { useBus } from '../bus/index.js';
|
|
4
|
+
const nodeId = nanoid(8);
|
|
5
|
+
export class LogsStream extends Writable {
|
|
6
|
+
messenger;
|
|
7
|
+
pretty;
|
|
8
|
+
constructor(pretty) {
|
|
9
|
+
super({ objectMode: true });
|
|
10
|
+
this.messenger = useBus();
|
|
11
|
+
this.pretty = pretty;
|
|
12
|
+
}
|
|
13
|
+
_write(chunk, _encoding, callback) {
|
|
14
|
+
if (!this.pretty) {
|
|
15
|
+
// keeping this string interpolation for performance on RAW logs
|
|
16
|
+
this.messenger.publish('logs', `{"log":${chunk},"nodeId":"${nodeId}"}`);
|
|
17
|
+
return callback();
|
|
18
|
+
}
|
|
19
|
+
const log = JSON.parse(chunk);
|
|
20
|
+
if (this.pretty === 'http' && log.req?.method && log.req?.url && log.res?.statusCode && log.responseTime) {
|
|
21
|
+
this.messenger.publish('logs', JSON.stringify({
|
|
22
|
+
log: {
|
|
23
|
+
level: log['level'],
|
|
24
|
+
time: log['time'],
|
|
25
|
+
msg: `${log.req.method} ${log.req.url} ${log.res.statusCode} ${log.responseTime}ms`,
|
|
26
|
+
},
|
|
27
|
+
nodeId: nodeId,
|
|
28
|
+
}));
|
|
29
|
+
return callback();
|
|
30
|
+
}
|
|
31
|
+
this.messenger.publish('logs', JSON.stringify({
|
|
32
|
+
log: {
|
|
33
|
+
level: log['level'],
|
|
34
|
+
time: log['time'],
|
|
35
|
+
msg: log['msg'],
|
|
36
|
+
},
|
|
37
|
+
nodeId: nodeId,
|
|
38
|
+
}));
|
|
39
|
+
callback();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -20,6 +20,7 @@ export const respond = asyncHandler(async (req, res) => {
|
|
|
20
20
|
exceedsMaxSize = valueSize > maxSize;
|
|
21
21
|
}
|
|
22
22
|
if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
|
|
23
|
+
req.originalUrl?.startsWith('/auth') === false &&
|
|
23
24
|
env['CACHE_ENABLED'] === true &&
|
|
24
25
|
cache &&
|
|
25
26
|
!req.sanitizedQuery.export &&
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
+
import { matches } from 'ip-matching';
|
|
3
4
|
import { useLogger } from '../logger/index.js';
|
|
4
5
|
import { ipInNetworks } from '../utils/ip-in-networks.js';
|
|
5
6
|
export function isDeniedIp(ip) {
|
|
@@ -24,8 +25,13 @@ export function isDeniedIp(ip) {
|
|
|
24
25
|
if (!networkInfo)
|
|
25
26
|
continue;
|
|
26
27
|
for (const info of networkInfo) {
|
|
27
|
-
if (info.
|
|
28
|
+
if (info.internal && info.cidr) {
|
|
29
|
+
if (matches(ip, info.cidr))
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
else if (info.address === ip) {
|
|
28
33
|
return true;
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
}
|
package/dist/server.js
CHANGED
|
@@ -14,7 +14,7 @@ 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
16
|
import { getAddress } from './utils/get-address.js';
|
|
17
|
-
import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
|
|
17
|
+
import { createLogsController, createSubscriptionController, createWebSocketController, getLogsController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
|
|
18
18
|
import { startWebSocketHandlers } from './websocket/handlers/index.js';
|
|
19
19
|
export let SERVER_ONLINE = true;
|
|
20
20
|
const env = useEnv();
|
|
@@ -77,6 +77,7 @@ export async function createServer() {
|
|
|
77
77
|
if (toBoolean(env['WEBSOCKETS_ENABLED']) === true) {
|
|
78
78
|
createSubscriptionController(server);
|
|
79
79
|
createWebSocketController(server);
|
|
80
|
+
createLogsController(server);
|
|
80
81
|
startWebSocketHandlers();
|
|
81
82
|
}
|
|
82
83
|
const terminusOptions = {
|
|
@@ -99,6 +100,7 @@ export async function createServer() {
|
|
|
99
100
|
async function onSignal() {
|
|
100
101
|
getSubscriptionController()?.terminate();
|
|
101
102
|
getWebSocketController()?.terminate();
|
|
103
|
+
getLogsController()?.terminate();
|
|
102
104
|
const database = getDatabase();
|
|
103
105
|
await database.destroy();
|
|
104
106
|
logger.info('Database connections destroyed');
|
|
@@ -126,7 +128,7 @@ export async function startServer() {
|
|
|
126
128
|
else {
|
|
127
129
|
listenOptions = {
|
|
128
130
|
host,
|
|
129
|
-
port: parseInt(port
|
|
131
|
+
port: parseInt(port),
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
server
|
|
@@ -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
|
}
|