@directus/api 23.1.0 → 23.1.2
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/controllers/permissions.js +1 -1
- package/dist/controllers/users.js +4 -8
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +2 -2
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mssql.js +1 -1
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mysql.js +2 -2
- package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/oracle.js +1 -1
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/postgres.js +3 -3
- package/dist/database/helpers/schema/types.d.ts +1 -1
- package/dist/database/helpers/schema/types.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.d.ts +0 -3
- package/dist/database/migrations/20240806A-permissions-policies.js +8 -94
- package/dist/database/run-ast/lib/get-db-query.js +1 -1
- package/dist/database/run-ast/utils/apply-case-when.js +1 -1
- package/dist/permissions/lib/fetch-permissions.d.ts +1 -1
- package/dist/permissions/lib/fetch-permissions.js +4 -1
- package/dist/permissions/modules/validate-access/validate-access.d.ts +1 -0
- package/dist/permissions/modules/validate-access/validate-access.js +1 -1
- package/dist/permissions/utils/fetch-share-info.d.ts +12 -0
- package/dist/permissions/utils/fetch-share-info.js +9 -0
- package/dist/permissions/utils/get-permissions-for-share.d.ts +4 -0
- package/dist/permissions/utils/get-permissions-for-share.js +182 -0
- package/dist/permissions/utils/merge-permissions.d.ts +9 -0
- package/dist/permissions/utils/merge-permissions.js +118 -0
- package/dist/services/assets.js +1 -1
- package/dist/services/authentication.js +1 -10
- package/dist/services/collections.js +5 -6
- package/dist/services/comments.js +8 -4
- package/dist/services/shares.d.ts +2 -0
- package/dist/services/shares.js +11 -9
- package/dist/types/auth.d.ts +0 -7
- package/dist/utils/get-accountability-for-token.js +0 -2
- package/package.json +20 -20
|
@@ -72,7 +72,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
|
|
72
72
|
router.get('/', validateBatch('read'), readHandler, respond);
|
|
73
73
|
router.search('/', validateBatch('read'), readHandler, respond);
|
|
74
74
|
router.get('/me', asyncHandler(async (req, res, next) => {
|
|
75
|
-
if (!req.accountability?.user && !req.accountability?.role)
|
|
75
|
+
if (!req.accountability?.user && !req.accountability?.role && !req.accountability?.share)
|
|
76
76
|
throw new ForbiddenError();
|
|
77
77
|
const result = await fetchAccountabilityCollectionAccess(req.accountability, {
|
|
78
78
|
schema: req.schema,
|
|
@@ -62,16 +62,12 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
|
|
62
62
|
router.get('/', validateBatch('read'), readHandler, respond);
|
|
63
63
|
router.search('/', validateBatch('read'), readHandler, respond);
|
|
64
64
|
router.get('/me', asyncHandler(async (req, res, next) => {
|
|
65
|
-
if (req.accountability?.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
id: req.accountability.role,
|
|
70
|
-
admin_access: false,
|
|
71
|
-
app_access: false,
|
|
65
|
+
if (req.accountability?.share) {
|
|
66
|
+
res.locals['payload'] = {
|
|
67
|
+
data: {
|
|
68
|
+
share: req.accountability?.share,
|
|
72
69
|
},
|
|
73
70
|
};
|
|
74
|
-
res.locals['payload'] = { data: user };
|
|
75
71
|
return next();
|
|
76
72
|
}
|
|
77
73
|
if (!req.accountability?.user) {
|
|
@@ -7,5 +7,5 @@ export declare class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
7
7
|
constraintName(existingName: string): string;
|
|
8
8
|
getDatabaseSize(): Promise<number | null>;
|
|
9
9
|
preprocessBindings(queryParams: Sql): Sql;
|
|
10
|
-
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[],
|
|
10
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
11
11
|
}
|
|
@@ -32,8 +32,8 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
32
32
|
preprocessBindings(queryParams) {
|
|
33
33
|
return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
|
|
34
34
|
}
|
|
35
|
-
addInnerSortFieldsToGroupBy(groupByFields, sortRecords,
|
|
36
|
-
if (
|
|
35
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
|
|
36
|
+
if (hasRelationalSort) {
|
|
37
37
|
/*
|
|
38
38
|
Cockroach allows aliases to be used in the GROUP BY clause and only needs columns in the GROUP BY clause that
|
|
39
39
|
are not functionally dependent on the primary key.
|
|
@@ -6,5 +6,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
6
6
|
formatUUID(uuid: string): string;
|
|
7
7
|
getDatabaseSize(): Promise<number | null>;
|
|
8
8
|
preprocessBindings(queryParams: Sql): Sql;
|
|
9
|
-
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[],
|
|
9
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
10
10
|
}
|
|
@@ -30,7 +30,7 @@ export class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
30
30
|
preprocessBindings(queryParams) {
|
|
31
31
|
return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
|
|
32
32
|
}
|
|
33
|
-
addInnerSortFieldsToGroupBy(groupByFields, sortRecords,
|
|
33
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
|
|
34
34
|
/*
|
|
35
35
|
MSSQL requires all selected columns that are not aggregated over are to be present in the GROUP BY clause
|
|
36
36
|
|
|
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord } from '../types.js';
|
|
|
3
3
|
export declare class SchemaHelperMySQL extends SchemaHelper {
|
|
4
4
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
5
5
|
getDatabaseSize(): Promise<number | null>;
|
|
6
|
-
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[],
|
|
6
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
7
7
|
}
|
|
@@ -28,8 +28,8 @@ export class SchemaHelperMySQL extends SchemaHelper {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
addInnerSortFieldsToGroupBy(groupByFields, sortRecords,
|
|
32
|
-
if (
|
|
31
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
|
|
32
|
+
if (hasRelationalSort) {
|
|
33
33
|
/*
|
|
34
34
|
** MySQL **
|
|
35
35
|
|
|
@@ -10,5 +10,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
|
|
|
10
10
|
processFieldType(field: Field): Type;
|
|
11
11
|
getDatabaseSize(): Promise<number | null>;
|
|
12
12
|
preprocessBindings(queryParams: Sql): Sql;
|
|
13
|
-
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[],
|
|
13
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
14
14
|
}
|
|
@@ -42,7 +42,7 @@ export class SchemaHelperOracle extends SchemaHelper {
|
|
|
42
42
|
preprocessBindings(queryParams) {
|
|
43
43
|
return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
|
|
44
44
|
}
|
|
45
|
-
addInnerSortFieldsToGroupBy(groupByFields, sortRecords,
|
|
45
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasRelationalSort) {
|
|
46
46
|
/*
|
|
47
47
|
Oracle requires all selected columns that are not aggregated over to be present in the GROUP BY clause
|
|
48
48
|
aliases can not be used before version 23c.
|
|
@@ -3,5 +3,5 @@ import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
|
|
|
3
3
|
export declare class SchemaHelperPostgres extends SchemaHelper {
|
|
4
4
|
getDatabaseSize(): Promise<number | null>;
|
|
5
5
|
preprocessBindings(queryParams: Sql): Sql;
|
|
6
|
-
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[],
|
|
6
|
+
addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasRelationalSort: boolean): void;
|
|
7
7
|
}
|
|
@@ -15,12 +15,12 @@ export class SchemaHelperPostgres extends SchemaHelper {
|
|
|
15
15
|
preprocessBindings(queryParams) {
|
|
16
16
|
return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
|
|
17
17
|
}
|
|
18
|
-
addInnerSortFieldsToGroupBy(groupByFields, sortRecords,
|
|
19
|
-
if (
|
|
18
|
+
addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasRelationalSort) {
|
|
19
|
+
if (hasRelationalSort) {
|
|
20
20
|
/*
|
|
21
21
|
Postgres only requires selected columns that are not functionally dependent on the primary key to be
|
|
22
22
|
included in the GROUP BY clause. Since the results are already grouped by the primary key, we don't need to
|
|
23
|
-
always include the sort columns in the GROUP BY but only if there is a
|
|
23
|
+
always include the sort columns in the GROUP BY but only if there is a relational sort involved, eg.
|
|
24
24
|
a sort column that comes from a related M2O relation.
|
|
25
25
|
|
|
26
26
|
> When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list
|
|
@@ -36,5 +36,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
36
36
|
*/
|
|
37
37
|
getDatabaseSize(): Promise<number | null>;
|
|
38
38
|
preprocessBindings(queryParams: Sql): Sql;
|
|
39
|
-
addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[],
|
|
39
|
+
addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasRelationalSort: boolean): void;
|
|
40
40
|
}
|
|
@@ -97,7 +97,7 @@ export class SchemaHelper extends DatabaseHelper {
|
|
|
97
97
|
preprocessBindings(queryParams) {
|
|
98
98
|
return queryParams;
|
|
99
99
|
}
|
|
100
|
-
addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords,
|
|
100
|
+
addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasRelationalSort) {
|
|
101
101
|
// no-op by default
|
|
102
102
|
}
|
|
103
103
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
|
-
import type { Permission } from '@directus/types';
|
|
3
|
-
export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): any[];
|
|
4
|
-
export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
|
|
5
2
|
export declare function up(knex: Knex): Promise<void>;
|
|
6
3
|
export declare function down(knex: Knex): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { processChunk, toBoolean } from '@directus/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { omit } from 'lodash-es';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { useLogger } from '../../logger/index.js';
|
|
5
5
|
import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
|
|
@@ -7,98 +7,7 @@ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
|
|
|
7
7
|
import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
|
|
8
8
|
import { getSchema } from '../../utils/get-schema.js';
|
|
9
9
|
import { getSchemaInspector } from '../index.js';
|
|
10
|
-
|
|
11
|
-
export function mergePermissions(strategy, ...permissions) {
|
|
12
|
-
const allPermissions = flatten(permissions);
|
|
13
|
-
const mergedPermissions = allPermissions
|
|
14
|
-
.reduce((acc, val) => {
|
|
15
|
-
const key = `${val.collection}__${val.action}`;
|
|
16
|
-
const current = acc.get(key);
|
|
17
|
-
acc.set(key, current ? mergePermission(strategy, current, val) : val);
|
|
18
|
-
return acc;
|
|
19
|
-
}, new Map())
|
|
20
|
-
.values();
|
|
21
|
-
return Array.from(mergedPermissions);
|
|
22
|
-
}
|
|
23
|
-
export function mergePermission(strategy, currentPerm, newPerm) {
|
|
24
|
-
const logicalKey = `_${strategy}`;
|
|
25
|
-
let { permissions, validation, fields, presets } = currentPerm;
|
|
26
|
-
if (newPerm.permissions) {
|
|
27
|
-
if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
|
|
28
|
-
permissions = {
|
|
29
|
-
[logicalKey]: [
|
|
30
|
-
...currentPerm.permissions[logicalKey],
|
|
31
|
-
newPerm.permissions,
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
else if (currentPerm.permissions) {
|
|
36
|
-
// Empty {} supersedes other permissions in _OR merge
|
|
37
|
-
if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
|
|
38
|
-
permissions = {};
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
permissions = {
|
|
42
|
-
[logicalKey]: [currentPerm.permissions, newPerm.permissions],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
permissions = {
|
|
48
|
-
[logicalKey]: [newPerm.permissions],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (newPerm.validation) {
|
|
53
|
-
if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
|
|
54
|
-
validation = {
|
|
55
|
-
[logicalKey]: [
|
|
56
|
-
...currentPerm.validation[logicalKey],
|
|
57
|
-
newPerm.validation,
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
else if (currentPerm.validation) {
|
|
62
|
-
// Empty {} supersedes other validations in _OR merge
|
|
63
|
-
if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
|
|
64
|
-
validation = {};
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
validation = {
|
|
68
|
-
[logicalKey]: [currentPerm.validation, newPerm.validation],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
validation = {
|
|
74
|
-
[logicalKey]: [newPerm.validation],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (newPerm.fields) {
|
|
79
|
-
if (Array.isArray(currentPerm.fields) && strategy === 'or') {
|
|
80
|
-
fields = uniq([...currentPerm.fields, ...newPerm.fields]);
|
|
81
|
-
}
|
|
82
|
-
else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
|
|
83
|
-
fields = intersection(currentPerm.fields, newPerm.fields);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
fields = newPerm.fields;
|
|
87
|
-
}
|
|
88
|
-
if (fields.includes('*'))
|
|
89
|
-
fields = ['*'];
|
|
90
|
-
}
|
|
91
|
-
if (newPerm.presets) {
|
|
92
|
-
presets = merge({}, presets, newPerm.presets);
|
|
93
|
-
}
|
|
94
|
-
return omit({
|
|
95
|
-
...currentPerm,
|
|
96
|
-
permissions,
|
|
97
|
-
validation,
|
|
98
|
-
fields,
|
|
99
|
-
presets,
|
|
100
|
-
}, ['id', 'system']);
|
|
101
|
-
}
|
|
10
|
+
import { mergePermissions } from '../../permissions/utils/merge-permissions.js';
|
|
102
11
|
async function fetchRoleAccess(roles, context) {
|
|
103
12
|
const roleAccess = {
|
|
104
13
|
admin_access: false,
|
|
@@ -286,7 +195,12 @@ export async function down(knex) {
|
|
|
286
195
|
const policies = await fetchPolicies({ roles: roleTree, user: null, ip: null }, context);
|
|
287
196
|
// fetch all of the policies permissions
|
|
288
197
|
const rawPermissions = await fetchPermissions({
|
|
289
|
-
accountability: {
|
|
198
|
+
accountability: {
|
|
199
|
+
role: null,
|
|
200
|
+
roles: roleTree,
|
|
201
|
+
user: null,
|
|
202
|
+
app: roleAccess?.app_access || false,
|
|
203
|
+
},
|
|
290
204
|
policies,
|
|
291
205
|
bypassDynamicVariableProcessing: true,
|
|
292
206
|
}, context);
|
|
@@ -181,7 +181,7 @@ export function getDBQuery({ table, fieldNodes, o2mNodes, query, cases, permissi
|
|
|
181
181
|
// Since the fields are expected to be the same for a single primary key it is safe to include them in the
|
|
182
182
|
// group by without influencing the result.
|
|
183
183
|
// This inclusion depends on the DB vendor, as such it is handled in a dialect specific helper.
|
|
184
|
-
helpers.schema.addInnerSortFieldsToGroupBy(groupByFields, innerQuerySortRecords, hasMultiRelationalSort ?? false);
|
|
184
|
+
helpers.schema.addInnerSortFieldsToGroupBy(groupByFields, innerQuerySortRecords, (hasMultiRelationalSort || sortRecords?.some(({ column }) => column.includes('.'))) ?? false);
|
|
185
185
|
dbQuery.groupBy(groupByFields);
|
|
186
186
|
}
|
|
187
187
|
const wrapperQuery = knex
|
|
@@ -16,7 +16,7 @@ export function applyCaseWhen({ columnCases, table, aliasMap, cases, column, ali
|
|
|
16
16
|
sqlParts.push(val);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
const sql = sqlParts.join(' ');
|
|
19
|
+
const sql = sqlParts.length > 0 ? sqlParts.join(' ') : '1';
|
|
20
20
|
const bindings = [...caseQuery.toSQL().bindings, column];
|
|
21
21
|
let rawCase = `(CASE WHEN ${sql} THEN ?? END)`;
|
|
22
22
|
if (alias) {
|
|
@@ -4,7 +4,7 @@ export interface FetchPermissionsOptions {
|
|
|
4
4
|
action?: PermissionsAction;
|
|
5
5
|
policies: string[];
|
|
6
6
|
collections?: string[];
|
|
7
|
-
accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app'>;
|
|
7
|
+
accountability?: Pick<Accountability, 'user' | 'role' | 'roles' | 'app' | 'share' | 'ip'>;
|
|
8
8
|
bypassDynamicVariableProcessing?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function fetchPermissions(options: FetchPermissionsOptions, context: Context): Promise<{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchDynamicVariableContext } from '../utils/fetch-dynamic-variable-context.js';
|
|
2
2
|
import { fetchRawPermissions } from '../utils/fetch-raw-permissions.js';
|
|
3
3
|
import { processPermissions } from '../utils/process-permissions.js';
|
|
4
|
+
import { getPermissionsForShare } from '../utils/get-permissions-for-share.js';
|
|
4
5
|
export async function fetchPermissions(options, context) {
|
|
5
6
|
const permissions = await fetchRawPermissions({ ...options, bypassMinimalAppPermissions: options.bypassDynamicVariableProcessing ?? false }, context);
|
|
6
7
|
if (options.accountability && !options.bypassDynamicVariableProcessing) {
|
|
@@ -15,7 +16,9 @@ export async function fetchPermissions(options, context) {
|
|
|
15
16
|
accountability: options.accountability,
|
|
16
17
|
permissionsContext,
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
+
if (options.accountability.share && (options.action === undefined || options.action === 'read')) {
|
|
20
|
+
return await getPermissionsForShare(options.accountability, options.collections, context);
|
|
21
|
+
}
|
|
19
22
|
return processedPermissions;
|
|
20
23
|
}
|
|
21
24
|
return permissions;
|
|
@@ -8,7 +8,7 @@ import { validateItemAccess } from './lib/validate-item-access.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export async function validateAccess(options, context) {
|
|
10
10
|
// Skip further validation if the collection does not exist
|
|
11
|
-
if (options.collection in context.schema.collections === false) {
|
|
11
|
+
if (!options.skipCollectionExistsCheck && options.collection in context.schema.collections === false) {
|
|
12
12
|
throw new ForbiddenError({
|
|
13
13
|
reason: `You don't have permission to "${options.action}" from collection "${options.collection}" or it does not exist.`,
|
|
14
14
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AbstractServiceOptions } from '../../types/services.js';
|
|
2
|
+
export interface ShareInfo {
|
|
3
|
+
collection: string;
|
|
4
|
+
item: string;
|
|
5
|
+
role: string | null;
|
|
6
|
+
user_created: {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare const fetchShareInfo: typeof _fetchShareInfo;
|
|
12
|
+
export declare function _fetchShareInfo(shareId: string, context: AbstractServiceOptions): Promise<ShareInfo>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { withCache } from './with-cache.js';
|
|
2
|
+
export const fetchShareInfo = withCache('share-info', _fetchShareInfo);
|
|
3
|
+
export async function _fetchShareInfo(shareId, context) {
|
|
4
|
+
const { SharesService } = await import('../../services/shares.js');
|
|
5
|
+
const sharesService = new SharesService(context);
|
|
6
|
+
return (await sharesService.readOne(shareId, {
|
|
7
|
+
fields: ['collection', 'item', 'role', 'user_created.id', 'user_created.role'],
|
|
8
|
+
}));
|
|
9
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Accountability, Permission, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Context } from '../types.js';
|
|
3
|
+
export declare function getPermissionsForShare(accountability: Pick<Accountability, 'share' | 'ip'>, collections: string[] | undefined, context: Context): Promise<Permission[]>;
|
|
4
|
+
export declare function traverse(schema: SchemaOverview, rootItemPrimaryKeyField: string, rootItemPrimaryKey: string, currentCollection: string, parentCollections?: string[], path?: string[]): Partial<Permission>[];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { schemaPermissions } from '@directus/system-data';
|
|
2
|
+
import { set, uniq } from 'lodash-es';
|
|
3
|
+
import { fetchAllowedFieldMap } from '../modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
|
|
4
|
+
import { fetchShareInfo } from './fetch-share-info.js';
|
|
5
|
+
import { mergePermissions } from './merge-permissions.js';
|
|
6
|
+
import { fetchPermissions } from '../lib/fetch-permissions.js';
|
|
7
|
+
import { fetchPolicies } from '../lib/fetch-policies.js';
|
|
8
|
+
import { fetchRolesTree } from '../lib/fetch-roles-tree.js';
|
|
9
|
+
import { reduceSchema } from '../../utils/reduce-schema.js';
|
|
10
|
+
import { fetchGlobalAccess } from '../modules/fetch-global-access/fetch-global-access.js';
|
|
11
|
+
export async function getPermissionsForShare(accountability, collections, context) {
|
|
12
|
+
const defaults = {
|
|
13
|
+
action: 'read',
|
|
14
|
+
collection: '',
|
|
15
|
+
permissions: {},
|
|
16
|
+
policy: null,
|
|
17
|
+
validation: null,
|
|
18
|
+
presets: null,
|
|
19
|
+
fields: null,
|
|
20
|
+
};
|
|
21
|
+
const { collection, item, role, user_created } = await fetchShareInfo(accountability.share, context);
|
|
22
|
+
const userAccountability = {
|
|
23
|
+
user: user_created.id,
|
|
24
|
+
role: user_created.role,
|
|
25
|
+
roles: await fetchRolesTree(user_created.role, context.knex),
|
|
26
|
+
admin: false,
|
|
27
|
+
app: false,
|
|
28
|
+
ip: accountability.ip,
|
|
29
|
+
};
|
|
30
|
+
// Fallback to public accountability so merging later on has no issues
|
|
31
|
+
const shareAccountability = {
|
|
32
|
+
user: null,
|
|
33
|
+
role: role,
|
|
34
|
+
roles: await fetchRolesTree(role, context.knex),
|
|
35
|
+
admin: false,
|
|
36
|
+
app: false,
|
|
37
|
+
ip: accountability.ip,
|
|
38
|
+
};
|
|
39
|
+
const [{ admin: shareIsAdmin }, { admin: userIsAdmin }, userPermissions, sharePermissions, shareFieldMap, userFieldMap,] = await Promise.all([
|
|
40
|
+
fetchGlobalAccess(shareAccountability, context.knex),
|
|
41
|
+
fetchGlobalAccess(userAccountability, context.knex),
|
|
42
|
+
getPermissionsForAccountability(userAccountability, context),
|
|
43
|
+
getPermissionsForAccountability(shareAccountability, context),
|
|
44
|
+
fetchAllowedFieldMap({
|
|
45
|
+
accountability: shareAccountability,
|
|
46
|
+
action: 'read',
|
|
47
|
+
}, context),
|
|
48
|
+
fetchAllowedFieldMap({
|
|
49
|
+
accountability: userAccountability,
|
|
50
|
+
action: 'read',
|
|
51
|
+
}, context),
|
|
52
|
+
]);
|
|
53
|
+
const isAdmin = userIsAdmin && shareIsAdmin;
|
|
54
|
+
let permissions = [];
|
|
55
|
+
let reducedSchema;
|
|
56
|
+
if (isAdmin) {
|
|
57
|
+
defaults.fields = ['*'];
|
|
58
|
+
reducedSchema = context.schema;
|
|
59
|
+
}
|
|
60
|
+
else if (userIsAdmin && !shareIsAdmin) {
|
|
61
|
+
permissions = sharePermissions;
|
|
62
|
+
reducedSchema = reduceSchema(context.schema, shareFieldMap);
|
|
63
|
+
}
|
|
64
|
+
else if (shareIsAdmin && !userIsAdmin) {
|
|
65
|
+
permissions = userPermissions;
|
|
66
|
+
reducedSchema = reduceSchema(context.schema, userFieldMap);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
permissions = mergePermissions('intersection', sharePermissions, userPermissions);
|
|
70
|
+
reducedSchema = reduceSchema(context.schema, shareFieldMap);
|
|
71
|
+
reducedSchema = reduceSchema(reducedSchema, userFieldMap);
|
|
72
|
+
}
|
|
73
|
+
const parentPrimaryKeyField = context.schema.collections[collection].primary;
|
|
74
|
+
const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection);
|
|
75
|
+
const parentCollectionPermission = {
|
|
76
|
+
...defaults,
|
|
77
|
+
collection,
|
|
78
|
+
permissions: {
|
|
79
|
+
[parentPrimaryKeyField]: {
|
|
80
|
+
_eq: item,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
// All permissions that will be merged into the original permissions set
|
|
85
|
+
const allGeneratedPermissions = [
|
|
86
|
+
parentCollectionPermission,
|
|
87
|
+
...relationalPermissions.map((generated) => ({ ...defaults, ...generated })),
|
|
88
|
+
...schemaPermissions,
|
|
89
|
+
];
|
|
90
|
+
// All the collections that are touched through the relational tree from the current root collection, and the schema collections
|
|
91
|
+
const allowedCollections = uniq(allGeneratedPermissions.map(({ collection }) => collection));
|
|
92
|
+
const generatedPermissions = [];
|
|
93
|
+
// Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve)
|
|
94
|
+
// the items of a collection if you entered that collection from multiple angles
|
|
95
|
+
for (const collection of allowedCollections) {
|
|
96
|
+
const permissionsForCollection = allGeneratedPermissions.filter((permission) => permission.collection === collection);
|
|
97
|
+
if (permissionsForCollection.length > 0) {
|
|
98
|
+
generatedPermissions.push(...mergePermissions('or', permissionsForCollection));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
generatedPermissions.push(...permissionsForCollection);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (isAdmin) {
|
|
105
|
+
return filterCollections(collections, generatedPermissions);
|
|
106
|
+
}
|
|
107
|
+
// Explicitly filter out permissions to collections unrelated to the root parent item.
|
|
108
|
+
const limitedPermissions = permissions.filter(({ action, collection }) => allowedCollections.includes(collection) && action === 'read');
|
|
109
|
+
return filterCollections(collections, mergePermissions('and', limitedPermissions, generatedPermissions));
|
|
110
|
+
}
|
|
111
|
+
function filterCollections(collections, permissions) {
|
|
112
|
+
if (!collections) {
|
|
113
|
+
return permissions;
|
|
114
|
+
}
|
|
115
|
+
return permissions.filter(({ collection }) => collections.includes(collection));
|
|
116
|
+
}
|
|
117
|
+
async function getPermissionsForAccountability(accountability, context) {
|
|
118
|
+
const policies = await fetchPolicies(accountability, context);
|
|
119
|
+
return fetchPermissions({
|
|
120
|
+
policies,
|
|
121
|
+
accountability,
|
|
122
|
+
}, context);
|
|
123
|
+
}
|
|
124
|
+
export function traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, currentCollection, parentCollections = [], path = []) {
|
|
125
|
+
const permissions = [];
|
|
126
|
+
// If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit.
|
|
127
|
+
// This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or
|
|
128
|
+
// articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁
|
|
129
|
+
if (parentCollections.includes(currentCollection)) {
|
|
130
|
+
return permissions;
|
|
131
|
+
}
|
|
132
|
+
const relationsInCollection = schema.relations.filter((relation) => {
|
|
133
|
+
return relation.collection === currentCollection || relation.related_collection === currentCollection;
|
|
134
|
+
});
|
|
135
|
+
for (const relation of relationsInCollection) {
|
|
136
|
+
let type;
|
|
137
|
+
if (relation.related_collection === currentCollection) {
|
|
138
|
+
type = 'o2m';
|
|
139
|
+
}
|
|
140
|
+
else if (!relation.related_collection) {
|
|
141
|
+
type = 'a2o';
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
type = 'm2o';
|
|
145
|
+
}
|
|
146
|
+
if (type === 'o2m') {
|
|
147
|
+
permissions.push({
|
|
148
|
+
collection: relation.collection,
|
|
149
|
+
permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
150
|
+
});
|
|
151
|
+
permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.collection, [...parentCollections, currentCollection], [...path, relation.field]));
|
|
152
|
+
}
|
|
153
|
+
if (type === 'a2o' && relation.meta?.one_allowed_collections) {
|
|
154
|
+
for (const collection of relation.meta.one_allowed_collections) {
|
|
155
|
+
permissions.push({
|
|
156
|
+
collection,
|
|
157
|
+
permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (type === 'm2o') {
|
|
162
|
+
permissions.push({
|
|
163
|
+
collection: relation.related_collection,
|
|
164
|
+
permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
165
|
+
});
|
|
166
|
+
if (relation.meta?.one_field) {
|
|
167
|
+
permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.related_collection, [...parentCollections, currentCollection], [...path, relation.meta?.one_field]));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return permissions;
|
|
172
|
+
}
|
|
173
|
+
function getFilterForPath(type, path, rootPrimaryKeyField, rootPrimaryKey) {
|
|
174
|
+
const filter = {};
|
|
175
|
+
if (type === 'm2o' || type === 'a2o') {
|
|
176
|
+
set(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
set(filter, path.reverse(), { _eq: rootPrimaryKey });
|
|
180
|
+
}
|
|
181
|
+
return filter;
|
|
182
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Permission } from '@directus/types';
|
|
2
|
+
/**
|
|
3
|
+
* Merges multiple permission lists into a flat list of unique permissions.
|
|
4
|
+
* @param strategy `and` or `or` deduplicate permissions while `intersection` makes sure only common permissions across all lists are kept and overlapping permissions are merged through `and`.
|
|
5
|
+
* @param permissions List of permission lists to merge.
|
|
6
|
+
* @returns A flat list of unique permissions.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mergePermissions(strategy: 'and' | 'or' | 'intersection', ...permissions: Permission[][]): Permission[];
|
|
9
|
+
export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
|
|
2
|
+
// Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
|
|
3
|
+
/**
|
|
4
|
+
* Merges multiple permission lists into a flat list of unique permissions.
|
|
5
|
+
* @param strategy `and` or `or` deduplicate permissions while `intersection` makes sure only common permissions across all lists are kept and overlapping permissions are merged through `and`.
|
|
6
|
+
* @param permissions List of permission lists to merge.
|
|
7
|
+
* @returns A flat list of unique permissions.
|
|
8
|
+
*/
|
|
9
|
+
export function mergePermissions(strategy, ...permissions) {
|
|
10
|
+
let allPermissions;
|
|
11
|
+
// Only keep permissions that are common to all lists
|
|
12
|
+
if (strategy === 'intersection') {
|
|
13
|
+
const permissionKeys = permissions.map((permissions) => {
|
|
14
|
+
return new Set(permissions.map((permission) => `${permission.collection}__${permission.action}`));
|
|
15
|
+
});
|
|
16
|
+
const intersectionKeys = permissionKeys.reduce((acc, val) => {
|
|
17
|
+
return new Set([...acc].filter((x) => val.has(x)));
|
|
18
|
+
}, permissionKeys[0]);
|
|
19
|
+
const deduplicateSubpermissions = permissions.map((permissions) => {
|
|
20
|
+
return mergePermissions('or', permissions);
|
|
21
|
+
});
|
|
22
|
+
allPermissions = flatten(deduplicateSubpermissions).filter((permission) => {
|
|
23
|
+
return intersectionKeys.has(`${permission.collection}__${permission.action}`);
|
|
24
|
+
});
|
|
25
|
+
strategy = 'and';
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
allPermissions = flatten(permissions);
|
|
29
|
+
}
|
|
30
|
+
const mergedPermissions = allPermissions
|
|
31
|
+
.reduce((acc, val) => {
|
|
32
|
+
const key = `${val.collection}__${val.action}`;
|
|
33
|
+
const current = acc.get(key);
|
|
34
|
+
acc.set(key, current ? mergePermission(strategy, current, val) : val);
|
|
35
|
+
return acc;
|
|
36
|
+
}, new Map())
|
|
37
|
+
.values();
|
|
38
|
+
return Array.from(mergedPermissions);
|
|
39
|
+
}
|
|
40
|
+
export function mergePermission(strategy, currentPerm, newPerm) {
|
|
41
|
+
const logicalKey = `_${strategy}`;
|
|
42
|
+
let { permissions, validation, fields, presets } = currentPerm;
|
|
43
|
+
if (newPerm.permissions) {
|
|
44
|
+
if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
|
|
45
|
+
permissions = {
|
|
46
|
+
[logicalKey]: [
|
|
47
|
+
...currentPerm.permissions[logicalKey],
|
|
48
|
+
newPerm.permissions,
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else if (currentPerm.permissions) {
|
|
53
|
+
// Empty {} supersedes other permissions in _OR merge
|
|
54
|
+
if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
|
|
55
|
+
permissions = {};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
permissions = {
|
|
59
|
+
[logicalKey]: [currentPerm.permissions, newPerm.permissions],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
permissions = {
|
|
65
|
+
[logicalKey]: [newPerm.permissions],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (newPerm.validation) {
|
|
70
|
+
if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
|
|
71
|
+
validation = {
|
|
72
|
+
[logicalKey]: [
|
|
73
|
+
...currentPerm.validation[logicalKey],
|
|
74
|
+
newPerm.validation,
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
else if (currentPerm.validation) {
|
|
79
|
+
// Empty {} supersedes other validations in _OR merge
|
|
80
|
+
if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
|
|
81
|
+
validation = {};
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
validation = {
|
|
85
|
+
[logicalKey]: [currentPerm.validation, newPerm.validation],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
validation = {
|
|
91
|
+
[logicalKey]: [newPerm.validation],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (newPerm.fields) {
|
|
96
|
+
if (Array.isArray(currentPerm.fields) && strategy === 'or') {
|
|
97
|
+
fields = uniq([...currentPerm.fields, ...newPerm.fields]);
|
|
98
|
+
}
|
|
99
|
+
else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
|
|
100
|
+
fields = intersection(currentPerm.fields, newPerm.fields);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
fields = newPerm.fields;
|
|
104
|
+
}
|
|
105
|
+
if (fields.includes('*'))
|
|
106
|
+
fields = ['*'];
|
|
107
|
+
}
|
|
108
|
+
if (newPerm.presets) {
|
|
109
|
+
presets = merge({}, presets, newPerm.presets);
|
|
110
|
+
}
|
|
111
|
+
return omit({
|
|
112
|
+
...currentPerm,
|
|
113
|
+
permissions,
|
|
114
|
+
validation,
|
|
115
|
+
fields,
|
|
116
|
+
presets,
|
|
117
|
+
}, ['id', 'system']);
|
|
118
|
+
}
|
package/dist/services/assets.js
CHANGED
|
@@ -121,7 +121,7 @@ export class AssetsService {
|
|
|
121
121
|
reason: 'Server too busy',
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
-
const version = file.modified_on !== undefined ? String(new Date(file.modified_on).getTime() / 1000) : undefined;
|
|
124
|
+
const version = file.modified_on !== undefined ? String(Math.round(new Date(file.modified_on).getTime() / 1000)) : undefined;
|
|
125
125
|
const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
|
|
126
126
|
const transformer = getSharpInstance();
|
|
127
127
|
transformer.timeout({
|
|
@@ -212,13 +212,8 @@ export class AuthenticationService {
|
|
|
212
212
|
user_auth_data: 'u.auth_data',
|
|
213
213
|
user_role: 'u.role',
|
|
214
214
|
share_id: 'd.id',
|
|
215
|
-
share_item: 'd.item',
|
|
216
|
-
share_role: 'd.role',
|
|
217
|
-
share_collection: 'd.collection',
|
|
218
215
|
share_start: 'd.date_start',
|
|
219
216
|
share_end: 'd.date_end',
|
|
220
|
-
share_times_used: 'd.times_used',
|
|
221
|
-
share_max_uses: 'd.max_uses',
|
|
222
217
|
})
|
|
223
218
|
.from('directus_sessions AS s')
|
|
224
219
|
.leftJoin('directus_users AS u', 's.user', 'u.id')
|
|
@@ -289,11 +284,7 @@ export class AuthenticationService {
|
|
|
289
284
|
}
|
|
290
285
|
if (record.share_id) {
|
|
291
286
|
tokenPayload.share = record.share_id;
|
|
292
|
-
tokenPayload.role =
|
|
293
|
-
tokenPayload.share_scope = {
|
|
294
|
-
collection: record.share_collection,
|
|
295
|
-
item: record.share_item,
|
|
296
|
-
};
|
|
287
|
+
tokenPayload.role = null;
|
|
297
288
|
tokenPayload.app_access = false;
|
|
298
289
|
tokenPayload.admin_access = false;
|
|
299
290
|
delete tokenPayload.id;
|
|
@@ -155,11 +155,10 @@ 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();
|
|
160
158
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
159
|
+
const updatedSchema = await getSchema();
|
|
161
160
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
162
|
-
nestedActionEvent.context.schema =
|
|
161
|
+
nestedActionEvent.context.schema = updatedSchema;
|
|
163
162
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
164
163
|
}
|
|
165
164
|
}
|
|
@@ -197,11 +196,10 @@ export class CollectionsService {
|
|
|
197
196
|
if (opts?.autoPurgeSystemCache !== false) {
|
|
198
197
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
199
198
|
}
|
|
200
|
-
// Refresh the schema for subsequent reads
|
|
201
|
-
this.schema = await getSchema();
|
|
202
199
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
200
|
+
const updatedSchema = await getSchema();
|
|
203
201
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
204
|
-
nestedActionEvent.context.schema =
|
|
202
|
+
nestedActionEvent.context.schema = updatedSchema;
|
|
205
203
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
206
204
|
}
|
|
207
205
|
}
|
|
@@ -290,6 +288,7 @@ export class CollectionsService {
|
|
|
290
288
|
accountability: this.accountability,
|
|
291
289
|
action: 'read',
|
|
292
290
|
collection,
|
|
291
|
+
skipCollectionExistsCheck: true,
|
|
293
292
|
}, {
|
|
294
293
|
schema: this.schema,
|
|
295
294
|
knex: this.knex,
|
|
@@ -5,6 +5,7 @@ import { cloneDeep, mergeWith, uniq } from 'lodash-es';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { useLogger } from '../logger/index.js';
|
|
7
7
|
import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
|
|
8
|
+
import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
|
|
8
9
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
9
10
|
import { isValidUuid } from '../utils/is-valid-uuid.js';
|
|
10
11
|
import { transaction } from '../utils/transaction.js';
|
|
@@ -118,16 +119,19 @@ export class CommentsService extends ItemsService {
|
|
|
118
119
|
for (const mention of mentions) {
|
|
119
120
|
const userID = mention.substring(1);
|
|
120
121
|
const user = await this.usersService.readOne(userID, {
|
|
121
|
-
fields: ['id', 'first_name', 'last_name', 'email', 'role.id'
|
|
122
|
+
fields: ['id', 'first_name', 'last_name', 'email', 'role.id'],
|
|
122
123
|
});
|
|
123
124
|
const accountability = {
|
|
124
125
|
user: userID,
|
|
125
126
|
role: user['role']?.id ?? null,
|
|
126
|
-
admin:
|
|
127
|
-
app:
|
|
128
|
-
roles: await fetchRolesTree(user['role']?.id, this.knex),
|
|
127
|
+
admin: false,
|
|
128
|
+
app: false,
|
|
129
|
+
roles: await fetchRolesTree(user['role']?.id ?? null, this.knex),
|
|
129
130
|
ip: null,
|
|
130
131
|
};
|
|
132
|
+
const userGlobalAccess = await fetchGlobalAccess(accountability, this.knex);
|
|
133
|
+
accountability.admin = userGlobalAccess.admin;
|
|
134
|
+
accountability.app = userGlobalAccess.app;
|
|
131
135
|
const usersService = new UsersService({ schema: this.schema, accountability });
|
|
132
136
|
try {
|
|
133
137
|
await validateAccess({
|
|
@@ -4,6 +4,8 @@ import { ItemsService } from './items.js';
|
|
|
4
4
|
export declare class SharesService extends ItemsService {
|
|
5
5
|
constructor(options: AbstractServiceOptions);
|
|
6
6
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
7
|
+
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
8
|
+
deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
7
9
|
login(payload: Record<string, any>, options?: Partial<{
|
|
8
10
|
session: boolean;
|
|
9
11
|
}>): Promise<Omit<LoginResult, 'id'>>;
|
package/dist/services/shares.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { ForbiddenError, InvalidCredentialsError } from '@directus/errors';
|
|
3
3
|
import argon2 from 'argon2';
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
|
+
import { nanoid } from 'nanoid';
|
|
5
6
|
import { useLogger } from '../logger/index.js';
|
|
6
7
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
7
8
|
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
@@ -12,6 +13,7 @@ import { userName } from '../utils/user-name.js';
|
|
|
12
13
|
import { ItemsService } from './items.js';
|
|
13
14
|
import { MailService } from './mail/index.js';
|
|
14
15
|
import { UsersService } from './users.js';
|
|
16
|
+
import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
|
|
15
17
|
const env = useEnv();
|
|
16
18
|
const logger = useLogger();
|
|
17
19
|
export class SharesService extends ItemsService {
|
|
@@ -32,14 +34,18 @@ export class SharesService extends ItemsService {
|
|
|
32
34
|
}
|
|
33
35
|
return super.createOne(data, opts);
|
|
34
36
|
}
|
|
37
|
+
async updateMany(keys, data, opts) {
|
|
38
|
+
await clearPermissionsCache();
|
|
39
|
+
return super.updateMany(keys, data, opts);
|
|
40
|
+
}
|
|
41
|
+
async deleteMany(keys, opts) {
|
|
42
|
+
await clearPermissionsCache();
|
|
43
|
+
return super.deleteMany(keys, opts);
|
|
44
|
+
}
|
|
35
45
|
async login(payload, options) {
|
|
36
|
-
const { nanoid } = await import('nanoid');
|
|
37
46
|
const record = await this.knex
|
|
38
47
|
.select({
|
|
39
48
|
share_id: 'id',
|
|
40
|
-
share_role: 'role',
|
|
41
|
-
share_item: 'item',
|
|
42
|
-
share_collection: 'collection',
|
|
43
49
|
share_start: 'date_start',
|
|
44
50
|
share_end: 'date_end',
|
|
45
51
|
share_times_used: 'times_used',
|
|
@@ -70,12 +76,8 @@ export class SharesService extends ItemsService {
|
|
|
70
76
|
const tokenPayload = {
|
|
71
77
|
app_access: false,
|
|
72
78
|
admin_access: false,
|
|
73
|
-
role:
|
|
79
|
+
role: null,
|
|
74
80
|
share: record.share_id,
|
|
75
|
-
share_scope: {
|
|
76
|
-
item: record.share_item,
|
|
77
|
-
collection: record.share_collection,
|
|
78
|
-
},
|
|
79
81
|
};
|
|
80
82
|
const refreshToken = nanoid(64);
|
|
81
83
|
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -31,16 +31,9 @@ export type DirectusTokenPayload = {
|
|
|
31
31
|
app_access: boolean | number;
|
|
32
32
|
admin_access: boolean | number;
|
|
33
33
|
share?: string;
|
|
34
|
-
share_scope?: {
|
|
35
|
-
collection: string;
|
|
36
|
-
item: string;
|
|
37
|
-
};
|
|
38
34
|
};
|
|
39
35
|
export type ShareData = {
|
|
40
36
|
share_id: string;
|
|
41
|
-
share_role: string;
|
|
42
|
-
share_item: string;
|
|
43
|
-
share_collection: string;
|
|
44
37
|
share_start: Date;
|
|
45
38
|
share_end: Date;
|
|
46
39
|
share_times_used: number;
|
|
@@ -21,8 +21,6 @@ export async function getAccountabilityForToken(token, accountability) {
|
|
|
21
21
|
}
|
|
22
22
|
if (payload.share)
|
|
23
23
|
accountability.share = payload.share;
|
|
24
|
-
if (payload.share_scope)
|
|
25
|
-
accountability.share_scope = payload.share_scope;
|
|
26
24
|
if (payload.id)
|
|
27
25
|
accountability.user = payload.id;
|
|
28
26
|
accountability.role = payload.role;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "23.1.
|
|
3
|
+
"version": "23.1.2",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -149,29 +149,29 @@
|
|
|
149
149
|
"ws": "8.18.0",
|
|
150
150
|
"zod": "3.23.8",
|
|
151
151
|
"zod-validation-error": "3.4.0",
|
|
152
|
+
"@directus/app": "13.3.2",
|
|
152
153
|
"@directus/constants": "12.0.0",
|
|
154
|
+
"@directus/env": "3.1.3",
|
|
153
155
|
"@directus/errors": "1.0.1",
|
|
154
|
-
"@directus/
|
|
155
|
-
"@directus/
|
|
156
|
-
"@directus/extensions-
|
|
157
|
-
"@directus/extensions-sdk": "12.1.1",
|
|
158
|
-
"@directus/extensions": "2.0.3",
|
|
156
|
+
"@directus/extensions-registry": "2.0.4",
|
|
157
|
+
"@directus/extensions": "2.0.4",
|
|
158
|
+
"@directus/extensions-sdk": "12.1.2",
|
|
159
159
|
"@directus/format-title": "11.0.0",
|
|
160
|
-
"@directus/memory": "2.0.
|
|
161
|
-
"@directus/pressure": "2.0.
|
|
162
|
-
"@directus/specs": "11.1.0",
|
|
160
|
+
"@directus/memory": "2.0.4",
|
|
161
|
+
"@directus/pressure": "2.0.3",
|
|
163
162
|
"@directus/schema": "12.1.1",
|
|
163
|
+
"@directus/specs": "11.1.0",
|
|
164
164
|
"@directus/storage": "11.0.1",
|
|
165
|
-
"@directus/storage-driver-azure": "11.0
|
|
166
|
-
"@directus/storage-driver-
|
|
167
|
-
"@directus/storage-driver-
|
|
165
|
+
"@directus/storage-driver-azure": "11.1.0",
|
|
166
|
+
"@directus/storage-driver-gcs": "11.1.0",
|
|
167
|
+
"@directus/storage-driver-cloudinary": "11.1.0",
|
|
168
168
|
"@directus/storage-driver-local": "11.0.1",
|
|
169
|
-
"@directus/storage-driver-s3": "11.0.
|
|
170
|
-
"@directus/storage-driver-supabase": "2.0
|
|
171
|
-
"@directus/system-data": "2.1.
|
|
172
|
-
"@directus/
|
|
173
|
-
"directus": "
|
|
174
|
-
"
|
|
169
|
+
"@directus/storage-driver-s3": "11.0.3",
|
|
170
|
+
"@directus/storage-driver-supabase": "2.1.0",
|
|
171
|
+
"@directus/system-data": "2.1.1",
|
|
172
|
+
"@directus/validation": "1.0.3",
|
|
173
|
+
"@directus/utils": "12.0.3",
|
|
174
|
+
"directus": "11.2.1"
|
|
175
175
|
},
|
|
176
176
|
"devDependencies": {
|
|
177
177
|
"@ngneat/falso": "7.2.0",
|
|
@@ -214,8 +214,8 @@
|
|
|
214
214
|
"typescript": "5.6.3",
|
|
215
215
|
"vitest": "2.1.2",
|
|
216
216
|
"@directus/random": "1.0.0",
|
|
217
|
-
"@directus/
|
|
218
|
-
"@directus/
|
|
217
|
+
"@directus/tsconfig": "2.0.0",
|
|
218
|
+
"@directus/types": "12.2.1"
|
|
219
219
|
},
|
|
220
220
|
"optionalDependencies": {
|
|
221
221
|
"@keyv/redis": "3.0.1",
|