directus 9.3.0 → 9.4.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 +5 -3
- package/dist/auth/auth.d.ts +4 -6
- package/dist/auth/auth.js +5 -9
- package/dist/auth/drivers/ldap.d.ts +3 -3
- package/dist/auth/drivers/ldap.js +0 -2
- package/dist/auth/drivers/local.d.ts +2 -2
- package/dist/auth/drivers/local.js +5 -12
- package/dist/auth/drivers/oauth2.d.ts +3 -3
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.d.ts +3 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cli/commands/schema/apply.js +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +16 -2
- package/dist/controllers/shares.d.ts +2 -0
- package/dist/controllers/shares.js +212 -0
- package/dist/controllers/users.js +21 -9
- package/dist/database/migrations/20211211A-add-shares.d.ts +3 -0
- package/dist/database/migrations/20211211A-add-shares.js +37 -0
- package/dist/database/run-ast.js +5 -5
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +0 -15
- package/dist/database/system-data/app-access-permissions/index.d.ts +1 -0
- package/dist/database/system-data/app-access-permissions/index.js +4 -2
- package/dist/database/system-data/app-access-permissions/schema-access-permissions.yaml +17 -0
- package/dist/database/system-data/collections/collections.yaml +3 -0
- package/dist/database/system-data/fields/sessions.yaml +1 -1
- package/dist/database/system-data/fields/shares.yaml +73 -0
- package/dist/database/system-data/fields/users.yaml +1 -1
- package/dist/database/system-data/relations/relations.yaml +15 -0
- package/dist/extensions.js +5 -3
- package/dist/middleware/authenticate.js +5 -15
- package/dist/middleware/check-ip.js +9 -6
- package/dist/middleware/respond.js +4 -1
- package/dist/services/activity.d.ts +2 -1
- package/dist/services/activity.js +2 -2
- package/dist/services/authentication.d.ts +2 -7
- package/dist/services/authentication.js +81 -41
- package/dist/services/authorization.js +3 -3
- package/dist/services/collections.d.ts +1 -2
- package/dist/services/collections.js +2 -2
- package/dist/services/files.d.ts +2 -2
- package/dist/services/graphql.js +16 -5
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.d.ts +1 -15
- package/dist/services/notifications.d.ts +2 -2
- package/dist/services/permissions.d.ts +2 -2
- package/dist/services/roles.d.ts +2 -2
- package/dist/services/shares.d.ts +17 -0
- package/dist/services/shares.js +135 -0
- package/dist/services/specifications.js +1 -1
- package/dist/services/users.d.ts +2 -2
- package/dist/services/webhooks.d.ts +2 -2
- package/dist/types/ast.d.ts +3 -3
- package/dist/types/auth.d.ts +31 -0
- package/dist/types/items.d.ts +14 -0
- package/dist/utils/apply-query.d.ts +0 -38
- package/dist/utils/apply-query.js +66 -67
- package/dist/utils/get-ast-from-query.js +3 -3
- package/dist/utils/get-permissions.js +15 -7
- package/dist/utils/get-relation-type.d.ts +1 -1
- package/dist/utils/get-relation-type.js +1 -1
- package/dist/utils/merge-permissions-for-share.d.ts +5 -0
- package/dist/utils/merge-permissions-for-share.js +116 -0
- package/dist/utils/merge-permissions.d.ts +13 -1
- package/dist/utils/merge-permissions.js +27 -19
- package/dist/utils/reduce-schema.d.ts +2 -2
- package/dist/utils/reduce-schema.js +7 -7
- package/dist/utils/user-name.js +3 -0
- package/package.json +12 -12
|
@@ -76,44 +76,44 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
|
|
|
76
76
|
return dbQuery;
|
|
77
77
|
}
|
|
78
78
|
exports.default = applyQuery;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
function getRelationInfo(relations, collection, field) {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
|
|
82
|
+
if (implicitRelation) {
|
|
83
|
+
if (implicitRelation[2] === undefined) {
|
|
84
|
+
const [m2oCollection, m2oField] = implicitRelation;
|
|
85
|
+
const relation = {
|
|
86
|
+
collection: m2oCollection,
|
|
87
|
+
field: m2oField,
|
|
88
|
+
related_collection: collection,
|
|
89
|
+
schema: null,
|
|
90
|
+
meta: null,
|
|
91
|
+
};
|
|
92
|
+
return { relation, relationType: 'o2m' };
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
|
|
96
|
+
const relation = {
|
|
97
|
+
collection: a2oCollection,
|
|
98
|
+
field: a2oItemField,
|
|
99
|
+
related_collection: collection,
|
|
100
|
+
schema: null,
|
|
101
|
+
meta: {
|
|
102
|
+
one_collection_field: a2oCollectionField,
|
|
103
|
+
one_field: field,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
return { relation, relationType: 'o2a' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const relation = (_b = relations.find((relation) => {
|
|
110
|
+
var _a;
|
|
111
|
+
return ((relation.collection === collection && relation.field === field) ||
|
|
112
|
+
(relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
|
|
113
|
+
})) !== null && _b !== void 0 ? _b : null;
|
|
114
|
+
const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
|
|
115
|
+
return { relation, relationType };
|
|
116
|
+
}
|
|
117
117
|
function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery = false) {
|
|
118
118
|
const helpers = (0, helpers_1.getHelpers)(knex);
|
|
119
119
|
const relations = schema.relations;
|
|
@@ -143,31 +143,34 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
143
143
|
followRelation(path);
|
|
144
144
|
function followRelation(pathParts, parentCollection = collection, parentAlias) {
|
|
145
145
|
/**
|
|
146
|
-
* For
|
|
146
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
147
147
|
*/
|
|
148
148
|
const pathRoot = pathParts[0].split(':')[0];
|
|
149
|
-
const relation = relations
|
|
150
|
-
|
|
151
|
-
return ((relation.collection === parentCollection && relation.field === pathRoot) ||
|
|
152
|
-
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
153
|
-
});
|
|
154
|
-
if (!relation)
|
|
149
|
+
const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
|
|
150
|
+
if (!relation) {
|
|
155
151
|
return;
|
|
156
|
-
|
|
152
|
+
}
|
|
157
153
|
const alias = generateAlias();
|
|
158
154
|
(0, lodash_1.set)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts, alias);
|
|
159
155
|
if (relationType === 'm2o') {
|
|
160
156
|
dbQuery.leftJoin({ [alias]: relation.related_collection }, `${parentAlias || parentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
|
|
161
157
|
}
|
|
162
|
-
if (relationType === '
|
|
158
|
+
if (relationType === 'a2o') {
|
|
163
159
|
const pathScope = pathParts[0].split(':')[1];
|
|
164
160
|
if (!pathScope) {
|
|
165
161
|
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
166
162
|
}
|
|
167
163
|
dbQuery.leftJoin({ [alias]: pathScope }, (joinClause) => {
|
|
168
164
|
joinClause
|
|
169
|
-
.
|
|
170
|
-
.
|
|
165
|
+
.onVal(relation.meta.one_collection_field, '=', pathScope)
|
|
166
|
+
.andOn(`${parentAlias || parentCollection}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${alias}.${schema.collections[pathScope].primary}`));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (relationType === 'o2a') {
|
|
170
|
+
dbQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
|
|
171
|
+
joinClause
|
|
172
|
+
.onVal(relation.meta.one_collection_field, '=', parentCollection)
|
|
173
|
+
.andOn(`${alias}.${relation.field}`, '=', knex.raw(`CAST(?? AS CHAR(255))`, `${parentAlias || parentCollection}.${schema.collections[parentCollection].primary}`));
|
|
171
174
|
});
|
|
172
175
|
}
|
|
173
176
|
// Still join o2m relations when in subquery OR when the o2m relation is not at the root level
|
|
@@ -179,7 +182,7 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
179
182
|
if (relationType === 'm2o') {
|
|
180
183
|
parent = relation.related_collection;
|
|
181
184
|
}
|
|
182
|
-
else if (relationType === '
|
|
185
|
+
else if (relationType === 'a2o') {
|
|
183
186
|
const pathScope = pathParts[0].split(':')[1];
|
|
184
187
|
if (!pathScope) {
|
|
185
188
|
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
@@ -215,17 +218,12 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
215
218
|
}
|
|
216
219
|
const filterPath = getFilterPath(key, value);
|
|
217
220
|
/**
|
|
218
|
-
* For
|
|
221
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
219
222
|
*/
|
|
220
223
|
const pathRoot = filterPath[0].split(':')[0];
|
|
221
|
-
const relation = relations
|
|
222
|
-
var _a;
|
|
223
|
-
return ((relation.collection === collection && relation.field === pathRoot) ||
|
|
224
|
-
(relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
225
|
-
});
|
|
224
|
+
const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
|
|
226
225
|
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
|
227
|
-
|
|
228
|
-
if (relationType === 'm2o' || relationType === 'm2a' || relationType === null) {
|
|
226
|
+
if (relationType === 'm2o' || relationType === 'a2o' || relationType === null) {
|
|
229
227
|
if (filterPath.length > 1) {
|
|
230
228
|
const columnName = getWhereColumn(filterPath, collection);
|
|
231
229
|
if (!columnName)
|
|
@@ -237,7 +235,13 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
else if (subQuery === false) {
|
|
240
|
-
|
|
238
|
+
if (!relation)
|
|
239
|
+
continue;
|
|
240
|
+
let pkField = `${collection}.${schema.collections[relation.related_collection].primary}`;
|
|
241
|
+
if (relationType === 'o2a') {
|
|
242
|
+
pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
|
|
243
|
+
}
|
|
244
|
+
// Note: knex's types don't appreciate knex.raw in whereIn, even though it's officially supported
|
|
241
245
|
dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
|
|
242
246
|
const field = relation.field;
|
|
243
247
|
const collection = relation.collection;
|
|
@@ -376,22 +380,17 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
|
|
|
376
380
|
return followRelation(path);
|
|
377
381
|
function followRelation(pathParts, parentCollection = collection, parentAlias) {
|
|
378
382
|
/**
|
|
379
|
-
* For
|
|
383
|
+
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
380
384
|
*/
|
|
381
385
|
const pathRoot = pathParts[0].split(':')[0];
|
|
382
|
-
const relation = relations
|
|
383
|
-
var _a;
|
|
384
|
-
return ((relation.collection === parentCollection && relation.field === pathRoot) ||
|
|
385
|
-
(relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === pathRoot));
|
|
386
|
-
});
|
|
386
|
+
const { relation, relationType } = getRelationInfo(relations, parentCollection, pathRoot);
|
|
387
387
|
if (!relation) {
|
|
388
388
|
throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
|
|
389
389
|
}
|
|
390
|
-
const relationType = (0, get_relation_type_1.getRelationType)({ relation, collection: parentCollection, field: pathRoot });
|
|
391
390
|
const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
|
|
392
391
|
const remainingParts = pathParts.slice(1);
|
|
393
392
|
let parent;
|
|
394
|
-
if (relationType === '
|
|
393
|
+
if (relationType === 'a2o') {
|
|
395
394
|
const pathScope = pathParts[0].split(':')[1];
|
|
396
395
|
if (!pathScope) {
|
|
397
396
|
throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when filtering on a many-to-any item`);
|
|
@@ -88,7 +88,7 @@ async function getASTFromQuery(collection, query, schema, options) {
|
|
|
88
88
|
const parts = name.split('.');
|
|
89
89
|
let rootField = parts[0];
|
|
90
90
|
let collectionScope = null;
|
|
91
|
-
//
|
|
91
|
+
// a2o related collection scoped field selector `fields=sections.section_id:headings.title`
|
|
92
92
|
if (rootField.includes(':')) {
|
|
93
93
|
const [key, scope] = rootField.split(':');
|
|
94
94
|
rootField = key;
|
|
@@ -136,14 +136,14 @@ async function getASTFromQuery(collection, query, schema, options) {
|
|
|
136
136
|
if (!relationType)
|
|
137
137
|
continue;
|
|
138
138
|
let child = null;
|
|
139
|
-
if (relationType === '
|
|
139
|
+
if (relationType === 'a2o') {
|
|
140
140
|
const allowedCollections = relation.meta.one_allowed_collections.filter((collection) => {
|
|
141
141
|
if (!permissions)
|
|
142
142
|
return true;
|
|
143
143
|
return permissions.some((permission) => permission.collection === collection);
|
|
144
144
|
});
|
|
145
145
|
child = {
|
|
146
|
-
type: '
|
|
146
|
+
type: 'a2o',
|
|
147
147
|
names: allowedCollections,
|
|
148
148
|
children: {},
|
|
149
149
|
query: {},
|
|
@@ -9,6 +9,7 @@ const lodash_1 = require("lodash");
|
|
|
9
9
|
const database_1 = __importDefault(require("../database"));
|
|
10
10
|
const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
|
|
11
11
|
const merge_permissions_1 = require("../utils/merge-permissions");
|
|
12
|
+
const merge_permissions_for_share_1 = require("./merge-permissions-for-share");
|
|
12
13
|
const users_1 = require("../services/users");
|
|
13
14
|
const roles_1 = require("../services/roles");
|
|
14
15
|
const cache_1 = require("../cache");
|
|
@@ -18,8 +19,8 @@ async function getPermissions(accountability, schema) {
|
|
|
18
19
|
const database = (0, database_1.default)();
|
|
19
20
|
const { systemCache, cache } = (0, cache_1.getCache)();
|
|
20
21
|
let permissions = [];
|
|
21
|
-
const { user, role, app, admin } = accountability;
|
|
22
|
-
const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin })}`;
|
|
22
|
+
const { user, role, app, admin, share_scope } = accountability;
|
|
23
|
+
const cacheKey = `permissions-${(0, object_hash_1.default)({ user, role, app, admin, share_scope })}`;
|
|
23
24
|
if (env_1.default.CACHE_PERMISSIONS !== false) {
|
|
24
25
|
const cachedPermissions = await systemCache.get(cacheKey);
|
|
25
26
|
if (cachedPermissions) {
|
|
@@ -44,14 +45,21 @@ async function getPermissions(accountability, schema) {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
if (accountability.admin !== true) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
48
|
+
const query = database.select('*').from('directus_permissions');
|
|
49
|
+
if (accountability.role) {
|
|
50
|
+
query.where({ role: accountability.role });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
query.whereNull('role');
|
|
54
|
+
}
|
|
55
|
+
const permissionsForRole = await query;
|
|
51
56
|
const { permissions: parsedPermissions, requiredPermissionData, containDynamicData, } = parsePermissions(permissionsForRole);
|
|
52
57
|
permissions = parsedPermissions;
|
|
53
58
|
if (accountability.app === true) {
|
|
54
|
-
permissions = (0, merge_permissions_1.mergePermissions)(permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
|
|
59
|
+
permissions = (0, merge_permissions_1.mergePermissions)('or', permissions, app_access_permissions_1.appAccessMinimalPermissions.map((perm) => ({ ...perm, role: accountability.role })));
|
|
60
|
+
}
|
|
61
|
+
if (accountability.share_scope) {
|
|
62
|
+
permissions = (0, merge_permissions_for_share_1.mergePermissionsForShare)(permissions, accountability, schema);
|
|
55
63
|
}
|
|
56
64
|
const filterContext = containDynamicData
|
|
57
65
|
? await getFilterContext(schema, accountability, requiredPermissionData)
|
|
@@ -10,7 +10,7 @@ function getRelationType(getRelationOptions) {
|
|
|
10
10
|
relation.field === field &&
|
|
11
11
|
((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_collection_field) &&
|
|
12
12
|
((_b = relation.meta) === null || _b === void 0 ? void 0 : _b.one_allowed_collections)) {
|
|
13
|
-
return '
|
|
13
|
+
return 'a2o';
|
|
14
14
|
}
|
|
15
15
|
if (relation.collection === collection && relation.field === field) {
|
|
16
16
|
return 'm2o';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Permission, Accountability, Filter } from '@directus/shared/types';
|
|
2
|
+
import { SchemaOverview } from '../types';
|
|
3
|
+
export declare function mergePermissionsForShare(currentPermissions: Permission[], accountability: Accountability, schema: SchemaOverview): Permission[];
|
|
4
|
+
export declare function traverse(schema: SchemaOverview, rootItemPrimaryKeyField: string, rootItemPrimaryKey: string, currentCollection: string, parentCollections?: string[], path?: string[]): Partial<Permission>[];
|
|
5
|
+
export declare function getFilterForPath(type: 'o2m' | 'm2o' | 'a2o', path: string[], rootPrimaryKeyField: string, rootPrimaryKey: string): Filter;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getFilterForPath = exports.traverse = exports.mergePermissionsForShare = void 0;
|
|
4
|
+
const lodash_1 = require("lodash");
|
|
5
|
+
const merge_permissions_1 = require("./merge-permissions");
|
|
6
|
+
const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
|
|
7
|
+
const reduce_schema_1 = require("./reduce-schema");
|
|
8
|
+
function mergePermissionsForShare(currentPermissions, accountability, schema) {
|
|
9
|
+
const defaults = {
|
|
10
|
+
action: 'read',
|
|
11
|
+
role: accountability.role,
|
|
12
|
+
collection: '',
|
|
13
|
+
permissions: {},
|
|
14
|
+
validation: null,
|
|
15
|
+
presets: null,
|
|
16
|
+
fields: null,
|
|
17
|
+
};
|
|
18
|
+
const { collection, item } = accountability.share_scope;
|
|
19
|
+
const parentPrimaryKeyField = schema.collections[collection].primary;
|
|
20
|
+
const reducedSchema = (0, reduce_schema_1.reduceSchema)(schema, currentPermissions, ['read']);
|
|
21
|
+
const relationalPermissions = traverse(reducedSchema, parentPrimaryKeyField, item, collection);
|
|
22
|
+
const parentCollectionPermission = (0, lodash_1.assign)({}, defaults, {
|
|
23
|
+
collection,
|
|
24
|
+
permissions: {
|
|
25
|
+
[parentPrimaryKeyField]: {
|
|
26
|
+
_eq: item,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
// All permissions that will be merged into the original permissions set
|
|
31
|
+
const allGeneratedPermissions = [
|
|
32
|
+
parentCollectionPermission,
|
|
33
|
+
...relationalPermissions.map((generated) => (0, lodash_1.assign)({}, defaults, generated)),
|
|
34
|
+
...app_access_permissions_1.schemaPermissions,
|
|
35
|
+
];
|
|
36
|
+
// All the collections that are touched through the relational tree from the current root collection, and the schema collections
|
|
37
|
+
const allowedCollections = (0, lodash_1.uniq)(allGeneratedPermissions.map(({ collection }) => collection));
|
|
38
|
+
const generatedPermissions = [];
|
|
39
|
+
// Merge all the permissions that relate to the same collection with an _or (this allows you to properly retrieve)
|
|
40
|
+
// the items of a collection if you entered that collection from multiple angles
|
|
41
|
+
for (const collection of allowedCollections) {
|
|
42
|
+
const permissionsForCollection = allGeneratedPermissions.filter((permission) => permission.collection === collection);
|
|
43
|
+
if (permissionsForCollection.length > 0) {
|
|
44
|
+
generatedPermissions.push(...(0, merge_permissions_1.mergePermissions)('or', permissionsForCollection));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
generatedPermissions.push(...permissionsForCollection);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Explicitly filter out permissions to collections unrelated to the root parent item.
|
|
51
|
+
const limitedPermissions = currentPermissions.filter(({ collection }) => allowedCollections.includes(collection));
|
|
52
|
+
return (0, merge_permissions_1.mergePermissions)('and', limitedPermissions, generatedPermissions);
|
|
53
|
+
}
|
|
54
|
+
exports.mergePermissionsForShare = mergePermissionsForShare;
|
|
55
|
+
function traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, currentCollection, parentCollections = [], path = []) {
|
|
56
|
+
var _a, _b, _c;
|
|
57
|
+
const permissions = [];
|
|
58
|
+
// If there's already a permissions rule for the collection we're currently checking, we'll shortcircuit.
|
|
59
|
+
// This prevents infinite loop in recursive relationships, like articles->related_articles->articles, or
|
|
60
|
+
// articles.author->users.avatar->files.created_by->users.avatar->files.created_by->🔁
|
|
61
|
+
if (parentCollections.includes(currentCollection)) {
|
|
62
|
+
return permissions;
|
|
63
|
+
}
|
|
64
|
+
const relationsInCollection = schema.relations.filter((relation) => {
|
|
65
|
+
return relation.collection === currentCollection || relation.related_collection === currentCollection;
|
|
66
|
+
});
|
|
67
|
+
for (const relation of relationsInCollection) {
|
|
68
|
+
let type;
|
|
69
|
+
if (relation.related_collection === currentCollection) {
|
|
70
|
+
type = 'o2m';
|
|
71
|
+
}
|
|
72
|
+
else if (!relation.related_collection) {
|
|
73
|
+
type = 'a2o';
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
type = 'm2o';
|
|
77
|
+
}
|
|
78
|
+
if (type === 'o2m') {
|
|
79
|
+
permissions.push({
|
|
80
|
+
collection: relation.collection,
|
|
81
|
+
permissions: getFilterForPath(type, [...path, relation.field], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
82
|
+
});
|
|
83
|
+
permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.collection, [...parentCollections, currentCollection], [...path, relation.field]));
|
|
84
|
+
}
|
|
85
|
+
if (type === 'a2o' && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_allowed_collections)) {
|
|
86
|
+
for (const collection of relation.meta.one_allowed_collections) {
|
|
87
|
+
permissions.push({
|
|
88
|
+
collection,
|
|
89
|
+
permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field},${relation.meta.one_collection_field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (type === 'm2o') {
|
|
94
|
+
permissions.push({
|
|
95
|
+
collection: relation.related_collection,
|
|
96
|
+
permissions: getFilterForPath(type, [...path, `$FOLLOW(${relation.collection},${relation.field})`], rootItemPrimaryKeyField, rootItemPrimaryKey),
|
|
97
|
+
});
|
|
98
|
+
if ((_b = relation.meta) === null || _b === void 0 ? void 0 : _b.one_field) {
|
|
99
|
+
permissions.push(...traverse(schema, rootItemPrimaryKeyField, rootItemPrimaryKey, relation.related_collection, [...parentCollections, currentCollection], [...path, (_c = relation.meta) === null || _c === void 0 ? void 0 : _c.one_field]));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return permissions;
|
|
104
|
+
}
|
|
105
|
+
exports.traverse = traverse;
|
|
106
|
+
function getFilterForPath(type, path, rootPrimaryKeyField, rootPrimaryKey) {
|
|
107
|
+
const filter = {};
|
|
108
|
+
if (type === 'm2o' || type === 'a2o') {
|
|
109
|
+
(0, lodash_1.set)(filter, path.reverse(), { [rootPrimaryKeyField]: { _eq: rootPrimaryKey } });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
(0, lodash_1.set)(filter, path.reverse(), { _eq: rootPrimaryKey });
|
|
113
|
+
}
|
|
114
|
+
return filter;
|
|
115
|
+
}
|
|
116
|
+
exports.getFilterForPath = getFilterForPath;
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
+
/// <reference types="lodash" />
|
|
1
2
|
import { Permission } from '@directus/shared/types';
|
|
2
|
-
export declare function mergePermissions(...permissions: Permission[][]): Permission[];
|
|
3
|
+
export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): Permission[];
|
|
4
|
+
export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): import("lodash").Omit<{
|
|
5
|
+
permissions: import("@directus/shared/types").Filter | null;
|
|
6
|
+
validation: import("@directus/shared/types").Filter | null;
|
|
7
|
+
fields: string[] | null;
|
|
8
|
+
presets: Record<string, any> | null;
|
|
9
|
+
id?: number | undefined;
|
|
10
|
+
role: string | null;
|
|
11
|
+
collection: string;
|
|
12
|
+
action: import("@directus/shared/types").PermissionsAction;
|
|
13
|
+
system?: true | undefined;
|
|
14
|
+
}, "id" | "system">;
|
|
@@ -1,66 +1,73 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.mergePermissions = void 0;
|
|
3
|
+
exports.mergePermission = exports.mergePermissions = void 0;
|
|
4
4
|
const lodash_1 = require("lodash");
|
|
5
|
-
function mergePermissions(...permissions) {
|
|
5
|
+
function mergePermissions(strategy, ...permissions) {
|
|
6
6
|
const allPermissions = (0, lodash_1.flatten)(permissions);
|
|
7
7
|
const mergedPermissions = allPermissions
|
|
8
8
|
.reduce((acc, val) => {
|
|
9
9
|
const key = `${val.collection}__${val.action}__${val.role || '$PUBLIC'}`;
|
|
10
10
|
const current = acc.get(key);
|
|
11
|
-
acc.set(key, current ?
|
|
11
|
+
acc.set(key, current ? mergePermission(strategy, current, val) : val);
|
|
12
12
|
return acc;
|
|
13
13
|
}, new Map())
|
|
14
14
|
.values();
|
|
15
|
-
|
|
16
|
-
return (0, lodash_1.omit)(perm, ['id', 'system']);
|
|
17
|
-
});
|
|
18
|
-
return result;
|
|
15
|
+
return Array.from(mergedPermissions);
|
|
19
16
|
}
|
|
20
17
|
exports.mergePermissions = mergePermissions;
|
|
21
|
-
function
|
|
18
|
+
function mergePermission(strategy, currentPerm, newPerm) {
|
|
19
|
+
const logicalKey = `_${strategy}`;
|
|
22
20
|
let permissions = currentPerm.permissions;
|
|
23
21
|
let validation = currentPerm.validation;
|
|
24
22
|
let fields = currentPerm.fields;
|
|
25
23
|
let presets = currentPerm.presets;
|
|
26
24
|
if (newPerm.permissions) {
|
|
27
|
-
if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] ===
|
|
25
|
+
if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
|
|
28
26
|
permissions = {
|
|
29
|
-
|
|
27
|
+
[logicalKey]: [
|
|
28
|
+
...currentPerm.permissions[logicalKey],
|
|
29
|
+
newPerm.permissions,
|
|
30
|
+
],
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
else if (currentPerm.permissions) {
|
|
33
34
|
permissions = {
|
|
34
|
-
|
|
35
|
+
[logicalKey]: [currentPerm.permissions, newPerm.permissions],
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
39
|
permissions = {
|
|
39
|
-
|
|
40
|
+
[logicalKey]: [newPerm.permissions],
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
if (newPerm.validation) {
|
|
44
|
-
if (currentPerm.validation && Object.keys(currentPerm.validation)[0] ===
|
|
45
|
+
if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
|
|
45
46
|
validation = {
|
|
46
|
-
|
|
47
|
+
[logicalKey]: [
|
|
48
|
+
...currentPerm.validation[logicalKey],
|
|
49
|
+
newPerm.validation,
|
|
50
|
+
],
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
else if (currentPerm.validation) {
|
|
50
54
|
validation = {
|
|
51
|
-
|
|
55
|
+
[logicalKey]: [currentPerm.validation, newPerm.validation],
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
else {
|
|
55
59
|
validation = {
|
|
56
|
-
|
|
60
|
+
[logicalKey]: [newPerm.validation],
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
if (newPerm.fields) {
|
|
61
|
-
if (Array.isArray(currentPerm.fields)) {
|
|
65
|
+
if (Array.isArray(currentPerm.fields) && strategy === 'or') {
|
|
62
66
|
fields = [...new Set([...currentPerm.fields, ...newPerm.fields])];
|
|
63
67
|
}
|
|
68
|
+
else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
|
|
69
|
+
fields = (0, lodash_1.intersection)(currentPerm.fields, newPerm.fields);
|
|
70
|
+
}
|
|
64
71
|
else {
|
|
65
72
|
fields = newPerm.fields;
|
|
66
73
|
}
|
|
@@ -70,11 +77,12 @@ function mergePerm(currentPerm, newPerm) {
|
|
|
70
77
|
if (newPerm.presets) {
|
|
71
78
|
presets = (0, lodash_1.merge)({}, presets, newPerm.presets);
|
|
72
79
|
}
|
|
73
|
-
return {
|
|
80
|
+
return (0, lodash_1.omit)({
|
|
74
81
|
...currentPerm,
|
|
75
82
|
permissions,
|
|
76
83
|
validation,
|
|
77
84
|
fields,
|
|
78
85
|
presets,
|
|
79
|
-
};
|
|
86
|
+
}, ['id', 'system']);
|
|
80
87
|
}
|
|
88
|
+
exports.mergePermission = mergePermission;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaOverview } from '../types';
|
|
2
|
-
import {
|
|
2
|
+
import { Permission, PermissionsAction } from '@directus/shared/types';
|
|
3
3
|
/**
|
|
4
4
|
* Reduces the schema based on the included permissions. The resulting object is the schema structure, but with only
|
|
5
5
|
* the allowed collections/fields/relations included based on the permissions.
|
|
@@ -7,4 +7,4 @@ import { Accountability, PermissionsAction } from '@directus/shared/types';
|
|
|
7
7
|
* @param actions Array of permissions actions (crud)
|
|
8
8
|
* @returns Reduced schema
|
|
9
9
|
*/
|
|
10
|
-
export declare function reduceSchema(schema: SchemaOverview,
|
|
10
|
+
export declare function reduceSchema(schema: SchemaOverview, permissions: Permission[] | null, actions?: PermissionsAction[]): SchemaOverview;
|
|
@@ -9,13 +9,13 @@ const lodash_1 = require("lodash");
|
|
|
9
9
|
* @param actions Array of permissions actions (crud)
|
|
10
10
|
* @returns Reduced schema
|
|
11
11
|
*/
|
|
12
|
-
function reduceSchema(schema,
|
|
13
|
-
var _a, _b, _c
|
|
12
|
+
function reduceSchema(schema, permissions, actions = ['create', 'read', 'update', 'delete']) {
|
|
13
|
+
var _a, _b, _c;
|
|
14
14
|
const reduced = {
|
|
15
15
|
collections: {},
|
|
16
16
|
relations: [],
|
|
17
17
|
};
|
|
18
|
-
const allowedFieldsInCollection = (
|
|
18
|
+
const allowedFieldsInCollection = (_a = permissions === null || permissions === void 0 ? void 0 : permissions.filter((permission) => actions.includes(permission.action)).reduce((acc, permission) => {
|
|
19
19
|
if (!acc[permission.collection]) {
|
|
20
20
|
acc[permission.collection] = [];
|
|
21
21
|
}
|
|
@@ -23,13 +23,13 @@ function reduceSchema(schema, accountability, actions = ['create', 'read', 'upda
|
|
|
23
23
|
acc[permission.collection] = (0, lodash_1.uniq)([...acc[permission.collection], ...permission.fields]);
|
|
24
24
|
}
|
|
25
25
|
return acc;
|
|
26
|
-
}, {})) !== null &&
|
|
26
|
+
}, {})) !== null && _a !== void 0 ? _a : {};
|
|
27
27
|
for (const [collectionName, collection] of Object.entries(schema.collections)) {
|
|
28
|
-
if (
|
|
28
|
+
if (permissions === null || permissions === void 0 ? void 0 : permissions.some((permission) => permission.collection === collectionName && actions.includes(permission.action))) {
|
|
29
29
|
const fields = {};
|
|
30
30
|
for (const [fieldName, field] of Object.entries(schema.collections[collectionName].fields)) {
|
|
31
|
-
if (((
|
|
32
|
-
((
|
|
31
|
+
if (((_b = allowedFieldsInCollection[collectionName]) === null || _b === void 0 ? void 0 : _b.includes('*')) ||
|
|
32
|
+
((_c = allowedFieldsInCollection[collectionName]) === null || _c === void 0 ? void 0 : _c.includes(fieldName))) {
|
|
33
33
|
fields[fieldName] = field;
|
|
34
34
|
}
|
|
35
35
|
}
|
package/dist/utils/user-name.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directus",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
4
4
|
"license": "GPL-3.0-only",
|
|
5
5
|
"homepage": "https://github.com/directus/directus#readme",
|
|
6
6
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
|
@@ -76,16 +76,16 @@
|
|
|
76
76
|
],
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@aws-sdk/client-ses": "^3.40.0",
|
|
79
|
-
"@directus/app": "9.
|
|
80
|
-
"@directus/drive": "9.
|
|
81
|
-
"@directus/drive-azure": "9.
|
|
82
|
-
"@directus/drive-gcs": "9.
|
|
83
|
-
"@directus/drive-s3": "9.
|
|
84
|
-
"@directus/extensions-sdk": "9.
|
|
85
|
-
"@directus/format-title": "9.
|
|
86
|
-
"@directus/schema": "9.
|
|
87
|
-
"@directus/shared": "9.
|
|
88
|
-
"@directus/specs": "9.
|
|
79
|
+
"@directus/app": "9.4.0",
|
|
80
|
+
"@directus/drive": "9.4.0",
|
|
81
|
+
"@directus/drive-azure": "9.4.0",
|
|
82
|
+
"@directus/drive-gcs": "9.4.0",
|
|
83
|
+
"@directus/drive-s3": "9.4.0",
|
|
84
|
+
"@directus/extensions-sdk": "9.4.0",
|
|
85
|
+
"@directus/format-title": "9.4.0",
|
|
86
|
+
"@directus/schema": "9.4.0",
|
|
87
|
+
"@directus/shared": "9.4.0",
|
|
88
|
+
"@directus/specs": "9.4.0",
|
|
89
89
|
"@godaddy/terminus": "^4.9.0",
|
|
90
90
|
"@rollup/plugin-alias": "^3.1.2",
|
|
91
91
|
"@rollup/plugin-virtual": "^2.0.3",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"sqlite3": "^5.0.2",
|
|
170
170
|
"tedious": "^13.0.0"
|
|
171
171
|
},
|
|
172
|
-
"gitHead": "
|
|
172
|
+
"gitHead": "a47b9cec0f24a7585108744b3c816bfc620297c4",
|
|
173
173
|
"devDependencies": {
|
|
174
174
|
"@types/async": "3.2.10",
|
|
175
175
|
"@types/atob": "2.1.2",
|