@directus/api 22.2.0 → 23.1.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 +2 -0
- package/dist/auth/drivers/ldap.js +14 -3
- package/dist/auth/drivers/oauth2.js +13 -2
- package/dist/auth/drivers/openid.js +13 -2
- package/dist/cache.js +4 -4
- package/dist/cli/commands/init/questions.d.ts +5 -5
- package/dist/cli/commands/schema/apply.d.ts +1 -0
- package/dist/cli/commands/schema/apply.js +20 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/utils/create-env/env-stub.liquid +1 -4
- package/dist/controllers/activity.js +30 -27
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/comments.d.ts +2 -0
- package/dist/controllers/comments.js +153 -0
- package/dist/controllers/versions.js +10 -5
- package/dist/database/index.js +3 -0
- package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
- package/dist/database/migrations/20240909A-separate-comments.d.ts +3 -0
- package/dist/database/migrations/20240909A-separate-comments.js +65 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.d.ts +3 -0
- package/dist/database/migrations/20240909B-consolidate-content-versioning.js +10 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +12 -2
- package/dist/database/run-ast/lib/get-db-query.js +2 -2
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.d.ts +15 -0
- package/dist/database/run-ast/modules/fetch-permitted-ast-root-fields.js +29 -0
- package/dist/database/run-ast/run-ast.js +8 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +10 -2
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
- package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
- package/dist/logger/index.d.ts +2 -3
- package/dist/logger/logs-stream.d.ts +0 -1
- package/dist/mailer.js +0 -6
- package/dist/middleware/authenticate.d.ts +1 -3
- package/dist/middleware/error-handler.d.ts +0 -1
- package/dist/middleware/validate-batch.d.ts +1 -4
- package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +2 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +18 -13
- package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
- package/dist/permissions/modules/validate-access/validate-access.js +14 -1
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
- package/dist/permissions/utils/process-permissions.d.ts +11 -1
- package/dist/permissions/utils/process-permissions.js +6 -4
- package/dist/request/agent-with-ip-validation.d.ts +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/services/activity.d.ts +1 -7
- package/dist/services/activity.js +0 -103
- package/dist/services/assets.d.ts +0 -1
- package/dist/services/assets.js +5 -4
- package/dist/services/collections.js +6 -4
- package/dist/services/comments.d.ts +31 -0
- package/dist/services/comments.js +374 -0
- package/dist/services/fields.js +0 -6
- package/dist/services/files/utils/get-metadata.d.ts +0 -1
- package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
- package/dist/services/files.d.ts +0 -1
- package/dist/services/graphql/index.js +17 -16
- package/dist/services/import-export.d.ts +0 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +3 -1
- package/dist/services/mail/index.d.ts +2 -1
- package/dist/services/mail/index.js +4 -1
- package/dist/services/payload.js +15 -14
- package/dist/services/tus/data-store.d.ts +0 -1
- package/dist/services/users.js +3 -2
- package/dist/services/versions.js +59 -44
- package/dist/types/graphql.d.ts +0 -1
- package/dist/utils/apply-diff.js +5 -6
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/compress.d.ts +0 -1
- package/dist/utils/delete-from-require-cache.js +1 -1
- package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
- package/dist/utils/generate-hash.js +2 -2
- package/dist/utils/get-address.d.ts +0 -3
- package/dist/utils/get-cache-headers.d.ts +0 -1
- package/dist/utils/get-cache-key.d.ts +0 -1
- package/dist/utils/get-column.d.ts +1 -1
- package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
- package/dist/utils/get-ip-from-req.d.ts +0 -1
- package/dist/utils/get-service.js +3 -1
- package/dist/utils/get-snapshot.js +1 -1
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/sanitize-schema.js +2 -0
- package/dist/utils/should-skip-cache.d.ts +0 -1
- package/dist/websocket/authenticate.js +1 -1
- package/dist/websocket/controllers/base.d.ts +1 -10
- package/dist/websocket/controllers/base.js +15 -21
- package/dist/websocket/controllers/graphql.d.ts +0 -3
- package/dist/websocket/controllers/index.d.ts +0 -3
- package/dist/websocket/controllers/logs.d.ts +4 -5
- package/dist/websocket/controllers/logs.js +7 -3
- package/dist/websocket/controllers/rest.d.ts +0 -3
- package/dist/websocket/controllers/rest.js +1 -1
- package/dist/websocket/types.d.ts +0 -6
- package/package.json +70 -71
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function up(knex) {
|
|
2
|
+
await knex.schema.alterTable('directus_versions', (table) => {
|
|
3
|
+
table.json('delta');
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export async function down(knex) {
|
|
7
|
+
await knex.schema.alterTable('directus_versions', (table) => {
|
|
8
|
+
table.dropColumn('delta');
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import type { Filter, Permission, Query
|
|
1
|
+
import type { Filter, Permission, Query } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
|
+
import type { Context } from '../../../permissions/types.js';
|
|
3
4
|
import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
|
|
4
|
-
export
|
|
5
|
+
export type DBQueryOptions = {
|
|
6
|
+
table: string;
|
|
7
|
+
fieldNodes: (FieldNode | FunctionFieldNode)[];
|
|
8
|
+
o2mNodes: O2MNode[];
|
|
9
|
+
query: Query;
|
|
10
|
+
cases: Filter[];
|
|
11
|
+
permissions: Permission[];
|
|
12
|
+
permissionsOnly?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }: DBQueryOptions, { knex, schema }: Context): Knex.QueryBuilder;
|
|
@@ -9,10 +9,10 @@ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
|
|
|
9
9
|
import { getNodeAlias } from '../utils/get-field-alias.js';
|
|
10
10
|
import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
|
|
11
11
|
import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
|
|
12
|
-
export function getDBQuery(
|
|
12
|
+
export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissions, permissionsOnly }, { knex, schema }) {
|
|
13
13
|
const aliasMap = Object.create(null);
|
|
14
14
|
const env = useEnv();
|
|
15
|
-
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap);
|
|
15
|
+
const preProcess = getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly);
|
|
16
16
|
const queryCopy = cloneDeep(query);
|
|
17
17
|
const helpers = getHelpers(knex);
|
|
18
18
|
const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import type { AST } from '../../../types/ast.js';
|
|
4
|
+
type FetchPermittedAstRootFieldsOptions = {
|
|
5
|
+
schema: SchemaOverview;
|
|
6
|
+
accountability: Accountability;
|
|
7
|
+
knex: Knex;
|
|
8
|
+
action: PermissionsAction;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
|
|
12
|
+
* same way as `runAst` but only returns flags (1/null) instead of actual field values.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchPermittedAstRootFields(originalAST: AST, { schema, accountability, knex, action }: FetchPermittedAstRootFieldsOptions): Promise<any>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash-es';
|
|
2
|
+
import { fetchPermissions } from '../../../permissions/lib/fetch-permissions.js';
|
|
3
|
+
import { fetchPolicies } from '../../../permissions/lib/fetch-policies.js';
|
|
4
|
+
import { getDBQuery } from '../lib/get-db-query.js';
|
|
5
|
+
import { parseCurrentLevel } from '../lib/parse-current-level.js';
|
|
6
|
+
/**
|
|
7
|
+
* Fetch the permitted top level fields of a given root type AST using a case/when query that is constructed the
|
|
8
|
+
* same way as `runAst` but only returns flags (1/null) instead of actual field values.
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchPermittedAstRootFields(originalAST, { schema, accountability, knex, action }) {
|
|
11
|
+
const ast = cloneDeep(originalAST);
|
|
12
|
+
const { name: collection, children, cases, query } = ast;
|
|
13
|
+
// Retrieve the database columns to select in the current AST
|
|
14
|
+
const { fieldNodes } = await parseCurrentLevel(schema, collection, children, query);
|
|
15
|
+
let permissions = [];
|
|
16
|
+
if (accountability && !accountability.admin) {
|
|
17
|
+
const policies = await fetchPolicies(accountability, { schema, knex });
|
|
18
|
+
permissions = await fetchPermissions({ action, accountability, policies }, { schema, knex });
|
|
19
|
+
}
|
|
20
|
+
return getDBQuery({
|
|
21
|
+
table: collection,
|
|
22
|
+
fieldNodes,
|
|
23
|
+
o2mNodes: [],
|
|
24
|
+
query,
|
|
25
|
+
cases,
|
|
26
|
+
permissions,
|
|
27
|
+
permissionsOnly: true,
|
|
28
|
+
}, { schema, knex });
|
|
29
|
+
}
|
|
@@ -36,7 +36,14 @@ export async function runAst(originalAST, schema, accountability, options) {
|
|
|
36
36
|
permissions = await fetchPermissions({ action: 'read', accountability, policies }, { schema, knex });
|
|
37
37
|
}
|
|
38
38
|
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
|
39
|
-
const dbQuery = getDBQuery(
|
|
39
|
+
const dbQuery = getDBQuery({
|
|
40
|
+
table: collection,
|
|
41
|
+
fieldNodes,
|
|
42
|
+
o2mNodes,
|
|
43
|
+
query,
|
|
44
|
+
cases,
|
|
45
|
+
permissions,
|
|
46
|
+
}, { schema, knex });
|
|
40
47
|
const rawItems = await dbQuery;
|
|
41
48
|
if (!rawItems)
|
|
42
49
|
return null;
|
|
@@ -6,5 +6,5 @@ interface NodePreProcessOptions {
|
|
|
6
6
|
/** Don't assign an alias to the column but instead return the column as is */
|
|
7
7
|
noAlias?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
9
|
+
export declare function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string, cases: Filter[], permissions: Permission[], aliasMap: AliasMap, permissionsOnly?: boolean): (fieldNode: FieldNode | FunctionFieldNode | M2ONode, options?: NodePreProcessOptions) => Knex.Raw<string>;
|
|
10
10
|
export {};
|
|
@@ -4,7 +4,7 @@ import { parseFilterKey } from '../../../utils/parse-filter-key.js';
|
|
|
4
4
|
import { getHelpers } from '../../helpers/index.js';
|
|
5
5
|
import { applyCaseWhen } from './apply-case-when.js';
|
|
6
6
|
import { getNodeAlias } from './get-field-alias.js';
|
|
7
|
-
export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap) {
|
|
7
|
+
export function getColumnPreprocessor(knex, schema, table, cases, permissions, aliasMap, permissionsOnly) {
|
|
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
|
|
@@ -22,7 +22,15 @@ export function getColumnPreprocessor(knex, schema, table, cases, permissions, a
|
|
|
22
22
|
field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
|
|
23
23
|
}
|
|
24
24
|
let column;
|
|
25
|
-
if (
|
|
25
|
+
if (permissionsOnly) {
|
|
26
|
+
if (noAlias) {
|
|
27
|
+
column = knex.raw(1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
column = knex.raw('1 as ??', [alias]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (field?.type?.startsWith('geometry')) {
|
|
26
34
|
column = helpers.st.asText(table, field.field, rawColumnAlias);
|
|
27
35
|
}
|
|
28
36
|
else if (fieldNode.type === 'functionField') {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
1
|
import type { Router } from 'express';
|
|
3
2
|
import type { Reference } from 'isolated-vm';
|
|
4
3
|
import type { IncomingHttpHeaders } from 'node:http';
|
|
5
4
|
export declare function registerRouteGenerator(endpointName: string, endpointRouter: Router): {
|
|
6
|
-
register: (path: Reference<string>, method: Reference<
|
|
5
|
+
register: (path: Reference<string>, method: Reference<"GET" | "POST" | "PUT" | "PATCH" | "DELETE">, cb: Reference<(req: {
|
|
7
6
|
url: string;
|
|
8
7
|
headers: IncomingHttpHeaders;
|
|
9
8
|
body: string;
|
package/dist/logger/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
1
|
import type { RequestHandler } from 'express';
|
|
3
2
|
import { type Logger } from 'pino';
|
|
4
3
|
import { LogsStream } from './logs-stream.js';
|
|
@@ -11,5 +10,5 @@ export declare const useLogger: () => Logger<never>;
|
|
|
11
10
|
export declare const getLogsStream: (pretty: boolean) => LogsStream;
|
|
12
11
|
export declare const getHttpLogsStream: (pretty: boolean) => LogsStream;
|
|
13
12
|
export declare const getLoggerLevelValue: (level: string) => number;
|
|
14
|
-
export declare const createLogger: () => Logger<never>;
|
|
15
|
-
export declare const createExpressLogger: () => RequestHandler
|
|
13
|
+
export declare const createLogger: () => Logger<never, boolean>;
|
|
14
|
+
export declare const createExpressLogger: () => RequestHandler;
|
package/dist/mailer.js
CHANGED
|
@@ -56,12 +56,6 @@ export default function getMailer() {
|
|
|
56
56
|
host: env['EMAIL_MAILGUN_HOST'] || 'api.mailgun.net',
|
|
57
57
|
}));
|
|
58
58
|
}
|
|
59
|
-
else if (transportName === 'sendgrid') {
|
|
60
|
-
const sg = require('nodemailer-sendgrid');
|
|
61
|
-
transporter = nodemailer.createTransport(sg({
|
|
62
|
-
apiKey: env['EMAIL_SENDGRID_API_KEY'],
|
|
63
|
-
}));
|
|
64
|
-
}
|
|
65
59
|
else {
|
|
66
60
|
logger.warn('Illegal transport given for email. Check the EMAIL_TRANSPORT env var.');
|
|
67
61
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
|
-
/// <reference types="cookie-parser" />
|
|
3
1
|
import type { NextFunction, Request, Response } from 'express';
|
|
4
2
|
/**
|
|
5
3
|
* Verify the passed JWT and assign the user ID and role to `req`
|
|
6
4
|
*/
|
|
7
5
|
export declare const handler: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8
|
-
declare const _default: (req: Request
|
|
6
|
+
declare const _default: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
9
7
|
export default _default;
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
/// <reference types="qs" />
|
|
2
1
|
import type { ErrorRequestHandler } from 'express';
|
|
3
2
|
export declare const errorHandler: (err: any, req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction) => Promise<ReturnType<ErrorRequestHandler>>;
|
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/// <reference types="express" />
|
|
3
|
-
/// <reference types="cookie-parser" />
|
|
4
|
-
export declare const validateBatch: (scope: 'read' | 'update' | 'delete') => (req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express").Response<any, Record<string, any>>, next: import("express").NextFunction) => Promise<void>;
|
|
1
|
+
export declare const validateBatch: (scope: "read" | "update" | "delete") => (req: import("express").Request, res: import("express").Response, next: import("express").NextFunction) => Promise<void>;
|
|
@@ -7,4 +7,14 @@ export interface FetchPermissionsOptions {
|
|
|
7
7
|
accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
|
|
8
8
|
bypassDynamicVariableProcessing?: boolean;
|
|
9
9
|
}
|
|
10
|
-
export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<
|
|
10
|
+
export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<{
|
|
11
|
+
permissions: import("@directus/types").Filter | null;
|
|
12
|
+
validation: import("@directus/types").Filter | null;
|
|
13
|
+
presets: any;
|
|
14
|
+
id?: number;
|
|
15
|
+
policy: string | null;
|
|
16
|
+
collection: string;
|
|
17
|
+
action: PermissionsAction;
|
|
18
|
+
fields: string[] | null;
|
|
19
|
+
system?: true;
|
|
20
|
+
}[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CollectionKey, FieldMap, QueryPath } from '../types.js';
|
|
2
2
|
export declare function getInfoForPath(fieldMap: FieldMap, group: keyof FieldMap, path: QueryPath, collection: CollectionKey): {
|
|
3
|
-
collection:
|
|
4
|
-
fields: Set<
|
|
3
|
+
collection: CollectionKey;
|
|
4
|
+
fields: Set<import("../types.js").FieldKey>;
|
|
5
5
|
};
|
|
@@ -5,5 +5,6 @@ export interface ValidateItemAccessOptions {
|
|
|
5
5
|
action: PermissionsAction;
|
|
6
6
|
collection: string;
|
|
7
7
|
primaryKeys: PrimaryKey[];
|
|
8
|
+
fields?: string[];
|
|
8
9
|
}
|
|
9
|
-
export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<
|
|
10
|
+
export declare function validateItemAccess(options: ValidateItemAccessOptions, context: Context): Promise<any>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runAst } from '../../../../database/run-ast/run-ast.js';
|
|
1
|
+
import { fetchPermittedAstRootFields } from '../../../../database/run-ast/modules/fetch-permitted-ast-root-fields.js';
|
|
3
2
|
import { processAst } from '../../process-ast/process-ast.js';
|
|
4
3
|
export async function validateItemAccess(options, context) {
|
|
5
4
|
const primaryKeyField = context.schema.collections[options.collection]?.primary;
|
|
@@ -8,17 +7,14 @@ export async function validateItemAccess(options, context) {
|
|
|
8
7
|
}
|
|
9
8
|
// When we're looking up access to specific items, we have to read them from the database to
|
|
10
9
|
// make sure you are allowed to access them.
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const ast = {
|
|
11
|
+
type: 'root',
|
|
12
|
+
name: options.collection,
|
|
13
|
+
query: { limit: options.primaryKeys.length },
|
|
14
|
+
// Act as if every field was a "normal" field
|
|
15
|
+
children: options.fields?.map((field) => ({ type: 'field', name: field, fieldKey: field, whenCase: [] })) ?? [],
|
|
16
|
+
cases: [],
|
|
16
17
|
};
|
|
17
|
-
const ast = await getAstFromQuery({
|
|
18
|
-
accountability: options.accountability,
|
|
19
|
-
query,
|
|
20
|
-
collection: options.collection,
|
|
21
|
-
}, context);
|
|
22
18
|
await processAst({ ast, ...options }, context);
|
|
23
19
|
// Inject the filter after the permissions have been processed, as to not require access to the primary key
|
|
24
20
|
ast.query.filter = {
|
|
@@ -26,8 +22,17 @@ export async function validateItemAccess(options, context) {
|
|
|
26
22
|
_in: options.primaryKeys,
|
|
27
23
|
},
|
|
28
24
|
};
|
|
29
|
-
const items = await
|
|
25
|
+
const items = await fetchPermittedAstRootFields(ast, {
|
|
26
|
+
schema: context.schema,
|
|
27
|
+
accountability: options.accountability,
|
|
28
|
+
knex: context.knex,
|
|
29
|
+
action: options.action,
|
|
30
|
+
});
|
|
30
31
|
if (items && items.length === options.primaryKeys.length) {
|
|
32
|
+
const { fields } = options;
|
|
33
|
+
if (fields) {
|
|
34
|
+
return items.every((item) => fields.every((field) => item[field] === 1));
|
|
35
|
+
}
|
|
31
36
|
return true;
|
|
32
37
|
}
|
|
33
38
|
return false;
|
|
@@ -7,6 +7,12 @@ import { validateItemAccess } from './lib/validate-item-access.js';
|
|
|
7
7
|
* control rules and checking if we got the expected result back
|
|
8
8
|
*/
|
|
9
9
|
export async function validateAccess(options, context) {
|
|
10
|
+
// Skip further validation if the collection does not exist
|
|
11
|
+
if (options.collection in context.schema.collections === false) {
|
|
12
|
+
throw new ForbiddenError({
|
|
13
|
+
reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
10
16
|
if (options.accountability.admin === true) {
|
|
11
17
|
return;
|
|
12
18
|
}
|
|
@@ -21,8 +27,15 @@ export async function validateAccess(options, context) {
|
|
|
21
27
|
access = await validateCollectionAccess(options, context);
|
|
22
28
|
}
|
|
23
29
|
if (!access) {
|
|
30
|
+
if (options.fields?.length ?? 0 > 0) {
|
|
31
|
+
throw new ForbiddenError({
|
|
32
|
+
reason: `You don't have permissions to perform "${options.action}" for the field(s) ${options
|
|
33
|
+
.fields.map((field) => `"${field}"`)
|
|
34
|
+
.join(', ')} in collection "${options.collection}" or it does not exist.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
24
37
|
throw new ForbiddenError({
|
|
25
|
-
reason: `You don't have permission to "${options.action}"
|
|
38
|
+
reason: `You don't have permission to perform "${options.action}" for collection "${options.collection}" or it does not exist.`,
|
|
26
39
|
});
|
|
27
40
|
}
|
|
28
41
|
}
|
package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type FetchUserCountOptions } from '../../../utils/fetch-user-count/fetch-user-count.js';
|
|
2
2
|
import type { Context } from '../../types.js';
|
|
3
|
-
export
|
|
4
|
-
}
|
|
3
|
+
export type ValidateRemainingAdminUsersOptions = Pick<FetchUserCountOptions, 'excludeAccessRows' | 'excludePolicies' | 'excludeUsers' | 'excludeRoles'>;
|
|
5
4
|
export declare function validateRemainingAdminUsers(options: ValidateRemainingAdminUsersOptions, context: Context): Promise<void>;
|
|
@@ -32,13 +32,21 @@ export async function fetchDynamicVariableContext(options, context) {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
-
if (options.policies.length > 0
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if (options.policies.length > 0) {
|
|
36
|
+
if ((permissionContext.$CURRENT_POLICIES?.size ?? 0) > 0) {
|
|
37
|
+
// Always add the id field
|
|
38
|
+
permissionContext.$CURRENT_POLICIES.add('id');
|
|
39
|
+
contextData['$CURRENT_POLICIES'] = await fetchContextData('$CURRENT_POLICIES', permissionContext, { policies: options.policies }, async (fields) => {
|
|
40
|
+
const policiesService = new PoliciesService(context);
|
|
41
|
+
return await policiesService.readMany(options.policies, {
|
|
42
|
+
fields,
|
|
43
|
+
});
|
|
40
44
|
});
|
|
41
|
-
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Always create entries for the policies with the `id` field present
|
|
48
|
+
contextData['$CURRENT_POLICIES'] = options.policies.map((id) => ({ id }));
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
return contextData;
|
|
44
52
|
}
|
|
@@ -4,4 +4,14 @@ export interface ProcessPermissionsOptions {
|
|
|
4
4
|
accountability: Pick<Accountability, 'user' | 'role' | 'roles'>;
|
|
5
5
|
permissionsContext: Record<string, any>;
|
|
6
6
|
}
|
|
7
|
-
export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions):
|
|
7
|
+
export declare function processPermissions({ permissions, accountability, permissionsContext }: ProcessPermissionsOptions): {
|
|
8
|
+
permissions: import("@directus/types").Filter | null;
|
|
9
|
+
validation: import("@directus/types").Filter | null;
|
|
10
|
+
presets: any;
|
|
11
|
+
id?: number;
|
|
12
|
+
policy: string | null;
|
|
13
|
+
collection: string;
|
|
14
|
+
action: import("@directus/types").PermissionsAction;
|
|
15
|
+
fields: string[] | null;
|
|
16
|
+
system?: true;
|
|
17
|
+
}[];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { parseFilter, parsePreset } from '@directus/utils';
|
|
2
2
|
export function processPermissions({ permissions, accountability, permissionsContext }) {
|
|
3
3
|
return permissions.map((permission) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
return {
|
|
5
|
+
...permission,
|
|
6
|
+
permissions: parseFilter(permission.permissions, accountability, permissionsContext),
|
|
7
|
+
validation: parseFilter(permission.validation, accountability, permissionsContext),
|
|
8
|
+
presets: parsePreset(permission.presets, accountability, permissionsContext),
|
|
9
|
+
};
|
|
8
10
|
});
|
|
9
11
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="node/http.js" />
|
|
3
|
-
/// <reference types="pino-http" />
|
|
4
1
|
import * as http from 'http';
|
|
5
2
|
export declare let SERVER_ONLINE: boolean;
|
|
6
3
|
export declare function createServer(): Promise<http.Server>;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
1
|
+
import type { AbstractServiceOptions } from '../types/index.js';
|
|
3
2
|
import { ItemsService } from './items.js';
|
|
4
|
-
import { NotificationsService } from './notifications.js';
|
|
5
|
-
import { UsersService } from './users.js';
|
|
6
3
|
export declare class ActivityService extends ItemsService {
|
|
7
|
-
notificationsService: NotificationsService;
|
|
8
|
-
usersService: UsersService;
|
|
9
4
|
constructor(options: AbstractServiceOptions);
|
|
10
|
-
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
11
5
|
}
|
|
@@ -1,109 +1,6 @@
|
|
|
1
|
-
import { Action } from '@directus/constants';
|
|
2
|
-
import { useEnv } from '@directus/env';
|
|
3
|
-
import { ErrorCode, isDirectusError } from '@directus/errors';
|
|
4
|
-
import { uniq } from 'lodash-es';
|
|
5
|
-
import { useLogger } from '../logger/index.js';
|
|
6
|
-
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
7
|
-
import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
|
|
8
|
-
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
9
|
-
import { createDefaultAccountability } from '../permissions/utils/create-default-accountability.js';
|
|
10
|
-
import { isValidUuid } from '../utils/is-valid-uuid.js';
|
|
11
|
-
import { Url } from '../utils/url.js';
|
|
12
|
-
import { userName } from '../utils/user-name.js';
|
|
13
1
|
import { ItemsService } from './items.js';
|
|
14
|
-
import { NotificationsService } from './notifications.js';
|
|
15
|
-
import { UsersService } from './users.js';
|
|
16
|
-
const env = useEnv();
|
|
17
|
-
const logger = useLogger();
|
|
18
2
|
export class ActivityService extends ItemsService {
|
|
19
|
-
notificationsService;
|
|
20
|
-
usersService;
|
|
21
3
|
constructor(options) {
|
|
22
4
|
super('directus_activity', options);
|
|
23
|
-
this.notificationsService = new NotificationsService({ schema: this.schema });
|
|
24
|
-
this.usersService = new UsersService({ schema: this.schema });
|
|
25
|
-
}
|
|
26
|
-
async createOne(data, opts) {
|
|
27
|
-
if (data['action'] === Action.COMMENT && typeof data['comment'] === 'string') {
|
|
28
|
-
const usersRegExp = new RegExp(/@[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/gi);
|
|
29
|
-
const mentions = uniq(data['comment'].match(usersRegExp) ?? []);
|
|
30
|
-
const sender = await this.usersService.readOne(this.accountability.user, {
|
|
31
|
-
fields: ['id', 'first_name', 'last_name', 'email'],
|
|
32
|
-
});
|
|
33
|
-
for (const mention of mentions) {
|
|
34
|
-
const userID = mention.substring(1);
|
|
35
|
-
const user = await this.usersService.readOne(userID, {
|
|
36
|
-
fields: ['id', 'first_name', 'last_name', 'email', 'role'],
|
|
37
|
-
});
|
|
38
|
-
const roles = await fetchRolesTree(user['role'], this.knex);
|
|
39
|
-
const globalAccess = await fetchGlobalAccess({ user: user['id'], roles, ip: null }, this.knex);
|
|
40
|
-
const accountability = createDefaultAccountability({
|
|
41
|
-
user: userID,
|
|
42
|
-
role: user['role']?.id ?? null,
|
|
43
|
-
roles,
|
|
44
|
-
...globalAccess,
|
|
45
|
-
});
|
|
46
|
-
const usersService = new UsersService({ schema: this.schema, accountability });
|
|
47
|
-
try {
|
|
48
|
-
if (this.accountability) {
|
|
49
|
-
await validateAccess({
|
|
50
|
-
accountability: this.accountability,
|
|
51
|
-
action: 'read',
|
|
52
|
-
collection: data['collection'],
|
|
53
|
-
primaryKeys: [data['item']],
|
|
54
|
-
}, {
|
|
55
|
-
knex: this.knex,
|
|
56
|
-
schema: this.schema,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
const templateData = await usersService.readByQuery({
|
|
60
|
-
fields: ['id', 'first_name', 'last_name', 'email'],
|
|
61
|
-
filter: { id: { _in: mentions.map((mention) => mention.substring(1)) } },
|
|
62
|
-
});
|
|
63
|
-
const userPreviews = templateData.reduce((acc, user) => {
|
|
64
|
-
acc[user['id']] = `<em>${userName(user)}</em>`;
|
|
65
|
-
return acc;
|
|
66
|
-
}, {});
|
|
67
|
-
let comment = data['comment'];
|
|
68
|
-
for (const mention of mentions) {
|
|
69
|
-
const uuid = mention.substring(1);
|
|
70
|
-
// We only match on UUIDs in the first place. This is just an extra sanity check.
|
|
71
|
-
if (isValidUuid(uuid) === false)
|
|
72
|
-
continue;
|
|
73
|
-
comment = comment.replace(new RegExp(mention, 'gm'), userPreviews[uuid] ?? '@Unknown User');
|
|
74
|
-
}
|
|
75
|
-
comment = `> ${comment.replace(/\n+/gm, '\n> ')}`;
|
|
76
|
-
const href = new Url(env['PUBLIC_URL'])
|
|
77
|
-
.addPath('admin', 'content', data['collection'], data['item'])
|
|
78
|
-
.toString();
|
|
79
|
-
const message = `
|
|
80
|
-
Hello ${userName(user)},
|
|
81
|
-
|
|
82
|
-
${userName(sender)} has mentioned you in a comment:
|
|
83
|
-
|
|
84
|
-
${comment}
|
|
85
|
-
|
|
86
|
-
<a href="${href}">Click here to view.</a>
|
|
87
|
-
`;
|
|
88
|
-
await this.notificationsService.createOne({
|
|
89
|
-
recipient: userID,
|
|
90
|
-
sender: sender['id'],
|
|
91
|
-
subject: `You were mentioned in ${data['collection']}`,
|
|
92
|
-
message,
|
|
93
|
-
collection: data['collection'],
|
|
94
|
-
item: data['item'],
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
if (isDirectusError(err, ErrorCode.Forbidden)) {
|
|
99
|
-
logger.warn(`User ${userID} doesn't have proper permissions to receive notification for this item.`);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return super.createOne(data, opts);
|
|
108
5
|
}
|
|
109
6
|
}
|
package/dist/services/assets.js
CHANGED
|
@@ -98,7 +98,7 @@ export class AssetsService {
|
|
|
98
98
|
}
|
|
99
99
|
if (exists) {
|
|
100
100
|
return {
|
|
101
|
-
stream: await storage.location(file.storage).read(assetFilename, range),
|
|
101
|
+
stream: await storage.location(file.storage).read(assetFilename, { range }),
|
|
102
102
|
file,
|
|
103
103
|
stat: await storage.location(file.storage).stat(assetFilename),
|
|
104
104
|
};
|
|
@@ -121,7 +121,8 @@ export class AssetsService {
|
|
|
121
121
|
reason: 'Server too busy',
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
-
const
|
|
124
|
+
const version = file.modified_on !== undefined ? String(new Date(file.modified_on).getTime() / 1000) : undefined;
|
|
125
|
+
const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
125
126
|
const transformer = getSharpInstance();
|
|
126
127
|
transformer.timeout({
|
|
127
128
|
seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
|
|
@@ -151,13 +152,13 @@ export class AssetsService {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
return {
|
|
154
|
-
stream: await storage.location(file.storage).read(assetFilename, range),
|
|
155
|
+
stream: await storage.location(file.storage).read(assetFilename, { range }),
|
|
155
156
|
stat: await storage.location(file.storage).stat(assetFilename),
|
|
156
157
|
file,
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
else {
|
|
160
|
-
const readStream = await storage.location(file.storage).read(file.filename_disk, range);
|
|
161
|
+
const readStream = await storage.location(file.storage).read(file.filename_disk, { range });
|
|
161
162
|
const stat = await storage.location(file.storage).stat(file.filename_disk);
|
|
162
163
|
return { stream: readStream, file, stat };
|
|
163
164
|
}
|
|
@@ -155,10 +155,11 @@ export class CollectionsService {
|
|
|
155
155
|
if (opts?.autoPurgeSystemCache !== false) {
|
|
156
156
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
157
157
|
}
|
|
158
|
+
// Refresh the schema for subsequent reads
|
|
159
|
+
this.schema = await getSchema();
|
|
158
160
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
159
|
-
const updatedSchema = await getSchema();
|
|
160
161
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
161
|
-
nestedActionEvent.context.schema =
|
|
162
|
+
nestedActionEvent.context.schema = this.schema;
|
|
162
163
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
163
164
|
}
|
|
164
165
|
}
|
|
@@ -196,10 +197,11 @@ export class CollectionsService {
|
|
|
196
197
|
if (opts?.autoPurgeSystemCache !== false) {
|
|
197
198
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
198
199
|
}
|
|
200
|
+
// Refresh the schema for subsequent reads
|
|
201
|
+
this.schema = await getSchema();
|
|
199
202
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
200
|
-
const updatedSchema = await getSchema();
|
|
201
203
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
202
|
-
nestedActionEvent.context.schema =
|
|
204
|
+
nestedActionEvent.context.schema = this.schema;
|
|
203
205
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
204
206
|
}
|
|
205
207
|
}
|