@directus/api 19.3.1 → 20.0.0-rc.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 +4 -4
- package/dist/auth/drivers/ldap.js +4 -4
- package/dist/auth/drivers/local.js +4 -4
- package/dist/auth/drivers/oauth2.js +4 -4
- package/dist/auth/drivers/openid.js +2 -4
- package/dist/cache.js +3 -0
- package/dist/cli/commands/bootstrap/index.js +8 -2
- package/dist/cli/commands/init/index.js +9 -10
- package/dist/cli/utils/defaults.d.ts +4 -11
- package/dist/cli/utils/defaults.js +7 -1
- package/dist/constants.d.ts +1 -1
- package/dist/controllers/access.d.ts +2 -0
- package/dist/controllers/access.js +148 -0
- package/dist/controllers/auth.js +5 -16
- package/dist/controllers/permissions.js +14 -2
- package/dist/controllers/policies.d.ts +2 -0
- package/dist/controllers/policies.js +169 -0
- package/dist/controllers/roles.js +22 -1
- package/dist/controllers/users.js +0 -55
- package/dist/database/errors/dialects/mysql.js +23 -23
- package/dist/database/get-ast-from-query/get-ast-from-query.d.ts +16 -0
- package/dist/database/get-ast-from-query/get-ast-from-query.js +82 -0
- package/dist/database/get-ast-from-query/lib/convert-wildcards.d.ts +13 -0
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +69 -0
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +15 -0
- package/dist/database/get-ast-from-query/lib/parse-fields.js +190 -0
- package/dist/database/get-ast-from-query/utils/get-deep-query.d.ts +14 -0
- package/dist/database/get-ast-from-query/utils/get-deep-query.js +17 -0
- package/dist/database/get-ast-from-query/utils/get-related-collection.d.ts +2 -0
- package/dist/database/get-ast-from-query/utils/get-related-collection.js +13 -0
- package/dist/database/get-ast-from-query/utils/get-relation.d.ts +2 -0
- package/dist/database/get-ast-from-query/utils/get-relation.js +7 -0
- package/dist/database/helpers/fn/types.d.ts +2 -1
- package/dist/database/helpers/fn/types.js +1 -1
- package/dist/database/helpers/geometry/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/geometry/dialects/mssql.js +4 -2
- package/dist/database/helpers/geometry/dialects/mysql.js +1 -1
- package/dist/database/helpers/geometry/dialects/oracle.d.ts +1 -1
- package/dist/database/helpers/geometry/dialects/oracle.js +5 -3
- package/dist/database/helpers/geometry/types.d.ts +1 -1
- package/dist/database/helpers/geometry/types.js +4 -2
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20240619A-permissions-policies.d.ts +3 -0
- package/dist/database/migrations/20240619A-permissions-policies.js +163 -0
- package/dist/database/run-ast/lib/get-db-query.d.ts +4 -0
- package/dist/database/run-ast/lib/get-db-query.js +194 -0
- package/dist/database/run-ast/lib/parse-current-level.d.ts +7 -0
- package/dist/database/run-ast/lib/parse-current-level.js +41 -0
- package/dist/database/run-ast/run-ast.d.ts +7 -0
- package/dist/database/run-ast/run-ast.js +107 -0
- package/dist/database/{run-ast.d.ts → run-ast/types.d.ts} +3 -9
- package/dist/database/run-ast/types.js +1 -0
- package/dist/database/run-ast/utils/apply-case-when.d.ts +16 -0
- package/dist/database/run-ast/utils/apply-case-when.js +26 -0
- package/dist/database/run-ast/utils/apply-parent-filters.d.ts +3 -0
- package/dist/database/run-ast/utils/apply-parent-filters.js +55 -0
- package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +10 -0
- package/dist/database/run-ast/utils/get-column-pre-processor.js +57 -0
- package/dist/database/run-ast/utils/get-field-alias.d.ts +2 -0
- package/dist/database/run-ast/utils/get-field-alias.js +4 -0
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +5 -0
- package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +23 -0
- package/dist/database/run-ast/utils/merge-with-parent-items.d.ts +3 -0
- package/dist/database/run-ast/utils/merge-with-parent-items.js +87 -0
- package/dist/database/run-ast/utils/remove-temporary-fields.d.ts +3 -0
- package/dist/database/run-ast/utils/remove-temporary-fields.js +73 -0
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
- package/dist/flows.js +3 -4
- package/dist/middleware/authenticate.js +2 -7
- package/dist/middleware/cache.js +1 -1
- package/dist/middleware/cors.js +4 -4
- package/dist/middleware/respond.js +1 -1
- package/dist/permissions/cache.d.ts +2 -0
- package/dist/permissions/cache.js +23 -0
- package/dist/permissions/lib/fetch-permissions.d.ts +10 -0
- package/dist/permissions/lib/fetch-permissions.js +55 -0
- package/dist/permissions/lib/fetch-policies.d.ts +7 -0
- package/dist/permissions/lib/fetch-policies.js +28 -0
- package/dist/permissions/lib/fetch-roles-tree.d.ts +3 -0
- package/dist/permissions/lib/fetch-roles-tree.js +28 -0
- package/dist/{services/permissions → permissions}/lib/with-app-minimal-permissions.d.ts +1 -1
- package/dist/permissions/lib/with-app-minimal-permissions.js +10 -0
- package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.d.ts +7 -0
- package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js +56 -0
- package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.d.ts +3 -0
- package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js +16 -0
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +8 -0
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +24 -0
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +9 -0
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +31 -0
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +16 -0
- package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +27 -0
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +10 -0
- package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +23 -0
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +5 -0
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +7 -0
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +5 -0
- package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +10 -0
- package/dist/permissions/modules/fetch-global-access/types.d.ts +4 -0
- package/dist/permissions/modules/fetch-global-access/types.js +1 -0
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +4 -0
- package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +27 -0
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +12 -0
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +32 -0
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +4 -0
- package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js +29 -0
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.d.ts +4 -0
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.js +49 -0
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.d.ts +3 -0
- package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.js +56 -0
- package/dist/permissions/modules/process-ast/lib/field-map-from-ast.d.ts +4 -0
- package/dist/permissions/modules/process-ast/lib/field-map-from-ast.js +8 -0
- package/dist/permissions/modules/process-ast/lib/inject-cases.d.ts +9 -0
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +93 -0
- package/dist/permissions/modules/process-ast/process-ast.d.ts +9 -0
- package/dist/permissions/modules/process-ast/process-ast.js +39 -0
- package/dist/permissions/modules/process-ast/types.d.ts +24 -0
- package/dist/permissions/modules/process-ast/types.js +1 -0
- package/dist/permissions/modules/process-ast/utils/collections-in-field-map.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/collections-in-field-map.js +7 -0
- package/dist/permissions/modules/process-ast/utils/dedupe-access.d.ts +12 -0
- package/dist/permissions/modules/process-ast/utils/dedupe-access.js +30 -0
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.d.ts +15 -0
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +50 -0
- package/dist/permissions/modules/process-ast/utils/find-related-collection.d.ts +3 -0
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +9 -0
- package/dist/permissions/modules/process-ast/utils/flatten-filter.d.ts +3 -0
- package/dist/permissions/modules/process-ast/utils/flatten-filter.js +24 -0
- package/dist/permissions/modules/process-ast/utils/format-a2o-key.d.ts +1 -0
- package/dist/permissions/modules/process-ast/utils/format-a2o-key.js +3 -0
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +5 -0
- package/dist/permissions/modules/process-ast/utils/get-info-for-path.js +7 -0
- package/dist/permissions/modules/process-ast/utils/has-item-permissions.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/has-item-permissions.js +3 -0
- package/dist/permissions/modules/process-ast/utils/stringify-query-path.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/stringify-query-path.js +3 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/create-error.d.ts +3 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/create-error.js +16 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.js +12 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.d.ts +2 -0
- package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.js +28 -0
- package/dist/permissions/modules/process-payload/lib/is-field-nullable.d.ts +5 -0
- package/dist/permissions/modules/process-payload/lib/is-field-nullable.js +12 -0
- package/dist/permissions/modules/process-payload/process-payload.d.ts +13 -0
- package/dist/permissions/modules/process-payload/process-payload.js +77 -0
- package/dist/permissions/modules/validate-access/lib/validate-collection-access.d.ts +12 -0
- package/dist/permissions/modules/validate-access/lib/validate-collection-access.js +11 -0
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +9 -0
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +33 -0
- package/dist/permissions/modules/validate-access/validate-access.d.ts +14 -0
- package/dist/permissions/modules/validate-access/validate-access.js +28 -0
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.d.ts +1 -0
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.js +8 -0
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +5 -0
- package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.js +10 -0
- package/dist/permissions/types.d.ts +6 -0
- package/dist/permissions/types.js +1 -0
- package/dist/permissions/utils/create-default-accountability.d.ts +2 -0
- package/dist/permissions/utils/create-default-accountability.js +11 -0
- package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +8 -0
- package/dist/permissions/utils/extract-required-dynamic-variable-context.js +27 -0
- package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +9 -0
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +43 -0
- package/dist/permissions/utils/filter-policies-by-ip.d.ts +2 -0
- package/dist/permissions/utils/filter-policies-by-ip.js +15 -0
- package/dist/permissions/utils/get-unaliased-field-key.d.ts +5 -0
- package/dist/permissions/utils/get-unaliased-field-key.js +17 -0
- package/dist/permissions/utils/process-permissions.d.ts +7 -0
- package/dist/permissions/utils/process-permissions.js +9 -0
- package/dist/permissions/utils/with-cache.d.ts +10 -0
- package/dist/permissions/utils/with-cache.js +25 -0
- package/dist/services/access.d.ts +10 -0
- package/dist/services/access.js +43 -0
- package/dist/services/activity.js +22 -10
- package/dist/services/assets.d.ts +2 -3
- package/dist/services/assets.js +10 -5
- package/dist/services/authentication.js +18 -18
- package/dist/services/collections.js +18 -17
- package/dist/services/fields.d.ts +0 -1
- package/dist/services/fields.js +53 -24
- package/dist/services/files.d.ts +0 -4
- package/dist/services/files.js +10 -10
- package/dist/services/flows.d.ts +0 -2
- package/dist/services/flows.js +2 -14
- package/dist/services/graphql/index.d.ts +3 -3
- package/dist/services/graphql/index.js +126 -22
- package/dist/services/graphql/subscription.js +2 -4
- package/dist/services/import-export.js +23 -9
- package/dist/services/index.d.ts +3 -2
- package/dist/services/index.js +3 -2
- package/dist/services/items.d.ts +40 -14
- package/dist/services/items.js +182 -79
- package/dist/services/meta.js +60 -23
- package/dist/services/notifications.d.ts +0 -1
- package/dist/services/notifications.js +0 -7
- package/dist/services/operations.d.ts +0 -2
- package/dist/services/operations.js +2 -14
- package/dist/services/payload.d.ts +9 -10
- package/dist/services/payload.js +35 -19
- package/dist/services/{permissions/index.d.ts → permissions.d.ts} +5 -7
- package/dist/services/{permissions/index.js → permissions.js} +30 -54
- package/dist/services/policies.d.ts +12 -0
- package/dist/services/policies.js +87 -0
- package/dist/services/relations.d.ts +0 -6
- package/dist/services/relations.js +26 -29
- package/dist/services/roles.d.ts +4 -14
- package/dist/services/roles.js +56 -430
- package/dist/services/shares.d.ts +0 -2
- package/dist/services/shares.js +12 -8
- package/dist/services/specifications.d.ts +2 -2
- package/dist/services/specifications.js +39 -27
- package/dist/services/users.d.ts +2 -20
- package/dist/services/users.js +87 -192
- package/dist/services/utils.js +11 -7
- package/dist/services/versions.d.ts +0 -2
- package/dist/services/versions.js +34 -10
- package/dist/telemetry/lib/get-report.js +6 -3
- package/dist/telemetry/types/report.d.ts +4 -0
- package/dist/telemetry/utils/check-user-limits.d.ts +5 -0
- package/dist/telemetry/utils/check-user-limits.js +19 -0
- package/dist/telemetry/utils/get-filesize-sum.d.ts +5 -0
- package/dist/telemetry/utils/get-filesize-sum.js +7 -0
- package/dist/types/ast.d.ts +43 -1
- package/dist/types/items.d.ts +11 -0
- package/dist/utils/apply-query.d.ts +4 -3
- package/dist/utils/apply-query.js +37 -8
- package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +17 -0
- package/dist/utils/fetch-user-count/fetch-access-lookup.js +22 -0
- package/dist/utils/fetch-user-count/fetch-access-roles.d.ts +16 -0
- package/dist/utils/fetch-user-count/fetch-access-roles.js +37 -0
- package/dist/utils/fetch-user-count/fetch-active-users.d.ts +6 -0
- package/dist/utils/fetch-user-count/fetch-active-users.js +3 -0
- package/dist/utils/fetch-user-count/fetch-user-count.d.ts +12 -0
- package/dist/utils/fetch-user-count/fetch-user-count.js +57 -0
- package/dist/utils/fetch-user-count/get-user-count-query.d.ts +20 -0
- package/dist/utils/fetch-user-count/get-user-count-query.js +17 -0
- package/dist/utils/get-accountability-for-role.js +16 -25
- package/dist/utils/get-accountability-for-token.js +17 -16
- package/dist/utils/get-cache-key.d.ts +1 -1
- package/dist/utils/get-cache-key.js +12 -1
- package/dist/utils/get-column.d.ts +2 -1
- package/dist/utils/get-column.js +1 -0
- package/dist/utils/get-graphql-type.js +1 -0
- package/dist/utils/get-service.d.ts +1 -1
- package/dist/utils/get-service.js +14 -10
- package/dist/utils/reduce-schema.d.ts +4 -6
- package/dist/utils/reduce-schema.js +14 -34
- package/dist/utils/validate-user-count-integrity.d.ts +13 -0
- package/dist/utils/validate-user-count-integrity.js +29 -0
- package/dist/websocket/authenticate.d.ts +0 -2
- package/dist/websocket/authenticate.js +0 -12
- package/dist/websocket/controllers/graphql.js +1 -4
- package/dist/websocket/controllers/hooks.js +4 -0
- package/dist/websocket/controllers/rest.js +0 -2
- package/dist/websocket/handlers/subscribe.js +0 -2
- package/dist/websocket/utils/items.d.ts +1 -1
- package/dist/websocket/utils/items.js +4 -1
- package/package.json +31 -30
- package/dist/database/run-ast.js +0 -450
- package/dist/middleware/check-ip.d.ts +0 -2
- package/dist/middleware/check-ip.js +0 -37
- package/dist/middleware/get-permissions.d.ts +0 -3
- package/dist/middleware/get-permissions.js +0 -10
- package/dist/services/authorization.d.ts +0 -17
- package/dist/services/authorization.js +0 -456
- package/dist/services/permissions/lib/with-app-minimal-permissions.js +0 -13
- package/dist/telemetry/utils/check-increased-user-limits.d.ts +0 -7
- package/dist/telemetry/utils/check-increased-user-limits.js +0 -22
- package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +0 -6
- package/dist/telemetry/utils/get-role-counts-by-roles.js +0 -27
- package/dist/telemetry/utils/get-role-counts-by-users.d.ts +0 -11
- package/dist/telemetry/utils/get-role-counts-by-users.js +0 -34
- package/dist/telemetry/utils/get-user-count.d.ts +0 -8
- package/dist/telemetry/utils/get-user-count.js +0 -33
- package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +0 -7
- package/dist/telemetry/utils/get-user-counts-by-roles.js +0 -35
- package/dist/utils/get-ast-from-query.d.ts +0 -13
- package/dist/utils/get-ast-from-query.js +0 -297
- package/dist/utils/get-permissions.d.ts +0 -2
- package/dist/utils/get-permissions.js +0 -150
- package/dist/utils/merge-permissions-for-share.d.ts +0 -4
- package/dist/utils/merge-permissions-for-share.js +0 -109
- package/dist/utils/merge-permissions.d.ts +0 -3
- package/dist/utils/merge-permissions.js +0 -95
package/dist/services/utils.js
CHANGED
|
@@ -3,6 +3,8 @@ import { systemCollectionRows } from '@directus/system-data';
|
|
|
3
3
|
import { clearSystemCache, getCache } from '../cache.js';
|
|
4
4
|
import getDatabase from '../database/index.js';
|
|
5
5
|
import emitter from '../emitter.js';
|
|
6
|
+
import { fetchAllowedFields } from '../permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js';
|
|
7
|
+
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
6
8
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
7
9
|
export class UtilsService {
|
|
8
10
|
knex;
|
|
@@ -20,14 +22,16 @@ export class UtilsService {
|
|
|
20
22
|
if (!sortField) {
|
|
21
23
|
throw new InvalidPayloadError({ reason: `Collection "${collection}" doesn't have a sort field` });
|
|
22
24
|
}
|
|
23
|
-
if (this.accountability
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (this.accountability && this.accountability.admin !== true) {
|
|
26
|
+
await validateAccess({
|
|
27
|
+
accountability: this.accountability,
|
|
28
|
+
action: 'update',
|
|
29
|
+
collection,
|
|
30
|
+
}, {
|
|
31
|
+
schema: this.schema,
|
|
32
|
+
knex: this.knex,
|
|
26
33
|
});
|
|
27
|
-
|
|
28
|
-
throw new ForbiddenError();
|
|
29
|
-
}
|
|
30
|
-
const allowedFields = permissions.fields ?? [];
|
|
34
|
+
const allowedFields = await fetchAllowedFields({ collection, action: 'update', accountability: this.accountability }, { schema: this.schema, knex: this.knex });
|
|
31
35
|
if (allowedFields[0] !== '*' && allowedFields.includes(sortField) === false) {
|
|
32
36
|
throw new ForbiddenError();
|
|
33
37
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Item, PrimaryKey, Query } from '@directus/types';
|
|
2
2
|
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
3
|
-
import { AuthorizationService } from './authorization.js';
|
|
4
3
|
import { ItemsService } from './items.js';
|
|
5
4
|
export declare class VersionsService extends ItemsService {
|
|
6
|
-
authorizationService: AuthorizationService;
|
|
7
5
|
constructor(options: AbstractServiceOptions);
|
|
8
6
|
private validateCreateData;
|
|
9
7
|
getMainItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
|
|
@@ -6,21 +6,15 @@ import objectHash from 'object-hash';
|
|
|
6
6
|
import { getCache } from '../cache.js';
|
|
7
7
|
import getDatabase from '../database/index.js';
|
|
8
8
|
import emitter from '../emitter.js';
|
|
9
|
+
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
9
10
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
10
11
|
import { ActivityService } from './activity.js';
|
|
11
|
-
import { AuthorizationService } from './authorization.js';
|
|
12
12
|
import { ItemsService } from './items.js';
|
|
13
13
|
import { PayloadService } from './payload.js';
|
|
14
14
|
import { RevisionsService } from './revisions.js';
|
|
15
15
|
export class VersionsService extends ItemsService {
|
|
16
|
-
authorizationService;
|
|
17
16
|
constructor(options) {
|
|
18
17
|
super('directus_versions', options);
|
|
19
|
-
this.authorizationService = new AuthorizationService({
|
|
20
|
-
accountability: this.accountability,
|
|
21
|
-
knex: this.knex,
|
|
22
|
-
schema: this.schema,
|
|
23
|
-
});
|
|
24
18
|
}
|
|
25
19
|
async validateCreateData(data) {
|
|
26
20
|
if (!data['key'])
|
|
@@ -55,11 +49,31 @@ export class VersionsService extends ItemsService {
|
|
|
55
49
|
});
|
|
56
50
|
}
|
|
57
51
|
// will throw an error if the accountability does not have permission to read the item
|
|
58
|
-
|
|
52
|
+
if (this.accountability) {
|
|
53
|
+
await validateAccess({
|
|
54
|
+
accountability: this.accountability,
|
|
55
|
+
action: 'read',
|
|
56
|
+
collection: data['collection'],
|
|
57
|
+
primaryKeys: [data['item']],
|
|
58
|
+
}, {
|
|
59
|
+
schema: this.schema,
|
|
60
|
+
knex: this.knex,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
async getMainItem(collection, item, query) {
|
|
61
65
|
// will throw an error if the accountability does not have permission to read the item
|
|
62
|
-
|
|
66
|
+
if (this.accountability) {
|
|
67
|
+
await validateAccess({
|
|
68
|
+
accountability: this.accountability,
|
|
69
|
+
action: 'read',
|
|
70
|
+
collection,
|
|
71
|
+
primaryKeys: [item],
|
|
72
|
+
}, {
|
|
73
|
+
schema: this.schema,
|
|
74
|
+
knex: this.knex,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
63
77
|
const itemsService = new ItemsService(collection, {
|
|
64
78
|
knex: this.knex,
|
|
65
79
|
accountability: this.accountability,
|
|
@@ -200,7 +214,17 @@ export class VersionsService extends ItemsService {
|
|
|
200
214
|
async promote(version, mainHash, fields) {
|
|
201
215
|
const { id, collection, item } = (await this.readOne(version));
|
|
202
216
|
// will throw an error if the accountability does not have permission to update the item
|
|
203
|
-
|
|
217
|
+
if (this.accountability) {
|
|
218
|
+
await validateAccess({
|
|
219
|
+
accountability: this.accountability,
|
|
220
|
+
action: 'update',
|
|
221
|
+
collection,
|
|
222
|
+
primaryKeys: [item],
|
|
223
|
+
}, {
|
|
224
|
+
schema: this.schema,
|
|
225
|
+
knex: this.knex,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
204
228
|
const { outdated } = await this.verifyHash(collection, item, mainHash);
|
|
205
229
|
if (outdated) {
|
|
206
230
|
throw new UnprocessableContentError({
|
|
@@ -2,10 +2,11 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { version } from 'directus/version';
|
|
3
3
|
import { getHelpers } from '../../database/helpers/index.js';
|
|
4
4
|
import { getDatabase, getDatabaseClient } from '../../database/index.js';
|
|
5
|
+
import { fetchUserCount } from '../../utils/fetch-user-count/fetch-user-count.js';
|
|
5
6
|
import { getExtensionCount } from '../utils/get-extension-count.js';
|
|
6
7
|
import { getFieldCount } from '../utils/get-field-count.js';
|
|
8
|
+
import { getFilesizeSum } from '../utils/get-filesize-sum.js';
|
|
7
9
|
import { getItemCount } from '../utils/get-item-count.js';
|
|
8
|
-
import { getUserCount } from '../utils/get-user-count.js';
|
|
9
10
|
import { getUserItemCount } from '../utils/get-user-item-count.js';
|
|
10
11
|
const basicCountTasks = [
|
|
11
12
|
{ collection: 'directus_dashboards' },
|
|
@@ -24,13 +25,14 @@ export const getReport = async () => {
|
|
|
24
25
|
const db = getDatabase();
|
|
25
26
|
const env = useEnv();
|
|
26
27
|
const helpers = getHelpers(db);
|
|
27
|
-
const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize] = await Promise.all([
|
|
28
|
+
const [basicCounts, userCounts, userItemCount, fieldsCounts, extensionsCounts, databaseSize, filesizes] = await Promise.all([
|
|
28
29
|
getItemCount(db, basicCountTasks),
|
|
29
|
-
|
|
30
|
+
fetchUserCount({ knex: db }),
|
|
30
31
|
getUserItemCount(db),
|
|
31
32
|
getFieldCount(db),
|
|
32
33
|
getExtensionCount(db),
|
|
33
34
|
helpers.schema.getDatabaseSize(),
|
|
35
|
+
getFilesizeSum(db),
|
|
34
36
|
]);
|
|
35
37
|
return {
|
|
36
38
|
url: env['PUBLIC_URL'],
|
|
@@ -50,5 +52,6 @@ export const getReport = async () => {
|
|
|
50
52
|
fields_total: fieldsCounts.total,
|
|
51
53
|
extensions: extensionsCounts.totalEnabled,
|
|
52
54
|
database_size: databaseSize ?? 0,
|
|
55
|
+
files_size_total: filesizes.total,
|
|
53
56
|
};
|
|
54
57
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { LimitExceededError } from '@directus/errors';
|
|
3
|
+
import {} from '../../utils/fetch-user-count/fetch-user-count.js';
|
|
4
|
+
const env = useEnv();
|
|
5
|
+
/**
|
|
6
|
+
* Ensure that user limits are not reached
|
|
7
|
+
*/
|
|
8
|
+
export async function checkUserLimits(userCounts) {
|
|
9
|
+
if (userCounts.admin > Number(env['USERS_ADMIN_ACCESS_LIMIT'])) {
|
|
10
|
+
throw new LimitExceededError({ category: 'Active Admin users' });
|
|
11
|
+
}
|
|
12
|
+
// Both app and admin users count against the app access limit
|
|
13
|
+
if (userCounts.app + userCounts.admin > Number(env['USERS_APP_ACCESS_LIMIT'])) {
|
|
14
|
+
throw new LimitExceededError({ category: 'Active App users' });
|
|
15
|
+
}
|
|
16
|
+
if (userCounts.api > Number(env['USERS_API_ACCESS_LIMIT'])) {
|
|
17
|
+
throw new LimitExceededError({ category: 'Active API users' });
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/types/ast.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Query, Relation } from '@directus/types';
|
|
1
|
+
import type { Filter, Query, Relation } from '@directus/types';
|
|
2
2
|
export type M2ONode = {
|
|
3
3
|
type: 'm2o';
|
|
4
4
|
name: string;
|
|
@@ -8,6 +8,14 @@ export type M2ONode = {
|
|
|
8
8
|
relation: Relation;
|
|
9
9
|
parentKey: string;
|
|
10
10
|
relatedKey: string;
|
|
11
|
+
/**
|
|
12
|
+
* Which permission cases have to be met on the current item for this field to return a value
|
|
13
|
+
*/
|
|
14
|
+
whenCase: number[];
|
|
15
|
+
/**
|
|
16
|
+
* Permissions rules for the item access of the children of this item.
|
|
17
|
+
*/
|
|
18
|
+
cases: Filter[];
|
|
11
19
|
};
|
|
12
20
|
export type A2MNode = {
|
|
13
21
|
type: 'a2o';
|
|
@@ -24,6 +32,16 @@ export type A2MNode = {
|
|
|
24
32
|
fieldKey: string;
|
|
25
33
|
relation: Relation;
|
|
26
34
|
parentKey: string;
|
|
35
|
+
/**
|
|
36
|
+
* Which permission cases have to be met on the current item for this field to return a value
|
|
37
|
+
*/
|
|
38
|
+
whenCase: number[];
|
|
39
|
+
/**
|
|
40
|
+
* Permissions rules for the item access of the children of this item.
|
|
41
|
+
*/
|
|
42
|
+
cases: {
|
|
43
|
+
[collection: string]: Filter[];
|
|
44
|
+
};
|
|
27
45
|
};
|
|
28
46
|
export type O2MNode = {
|
|
29
47
|
type: 'o2m';
|
|
@@ -34,12 +52,24 @@ export type O2MNode = {
|
|
|
34
52
|
relation: Relation;
|
|
35
53
|
parentKey: string;
|
|
36
54
|
relatedKey: string;
|
|
55
|
+
/**
|
|
56
|
+
* Which permission cases have to be met on the current item for this field to return a value
|
|
57
|
+
*/
|
|
58
|
+
whenCase: number[];
|
|
59
|
+
/**
|
|
60
|
+
* Permissions rules for the item access of the children of this item.
|
|
61
|
+
*/
|
|
62
|
+
cases: Filter[];
|
|
37
63
|
};
|
|
38
64
|
export type NestedCollectionNode = M2ONode | O2MNode | A2MNode;
|
|
39
65
|
export type FieldNode = {
|
|
40
66
|
type: 'field';
|
|
41
67
|
name: string;
|
|
42
68
|
fieldKey: string;
|
|
69
|
+
/**
|
|
70
|
+
* Which permission cases have to be met on the current item for this field to return a value
|
|
71
|
+
*/
|
|
72
|
+
whenCase: number[];
|
|
43
73
|
};
|
|
44
74
|
export type FunctionFieldNode = {
|
|
45
75
|
type: 'functionField';
|
|
@@ -47,10 +77,22 @@ export type FunctionFieldNode = {
|
|
|
47
77
|
fieldKey: string;
|
|
48
78
|
query: Query;
|
|
49
79
|
relatedCollection: string;
|
|
80
|
+
/**
|
|
81
|
+
* Which permission cases have to be met on the current item for this field to return a value
|
|
82
|
+
*/
|
|
83
|
+
whenCase: number[];
|
|
84
|
+
/**
|
|
85
|
+
* Permissions rules for the item access of the related collection of this item.
|
|
86
|
+
*/
|
|
87
|
+
cases: Filter[];
|
|
50
88
|
};
|
|
51
89
|
export type AST = {
|
|
52
90
|
type: 'root';
|
|
53
91
|
name: string;
|
|
54
92
|
children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[];
|
|
55
93
|
query: Query;
|
|
94
|
+
/**
|
|
95
|
+
* Permissions rules for the item access of the children of this item.
|
|
96
|
+
*/
|
|
97
|
+
cases: Filter[];
|
|
56
98
|
};
|
package/dist/types/items.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DirectusError } from '@directus/errors';
|
|
2
2
|
import type { EventContext, PrimaryKey } from '@directus/types';
|
|
3
3
|
import type { MutationTracker } from '../services/items.js';
|
|
4
|
+
import type { UserIntegrityCheckFlag } from '../utils/validate-user-count-integrity.js';
|
|
4
5
|
export type MutationOptions = {
|
|
5
6
|
/**
|
|
6
7
|
* Callback function that's fired whenever a revision is made in the mutation
|
|
@@ -33,6 +34,16 @@ export type MutationOptions = {
|
|
|
33
34
|
mutationTracker?: MutationTracker | undefined;
|
|
34
35
|
preMutationError?: DirectusError | undefined;
|
|
35
36
|
bypassAutoIncrementSequenceReset?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Indicate that the top level mutation needs to perform a user integrity check before commiting the transaction
|
|
39
|
+
* This is a combination of flags
|
|
40
|
+
* @see UserIntegrityCheckFlag
|
|
41
|
+
*/
|
|
42
|
+
userIntegrityCheckFlags?: UserIntegrityCheckFlag;
|
|
43
|
+
/**
|
|
44
|
+
* Callback function that is called whenever a mutation requires a user integrity check to be made
|
|
45
|
+
*/
|
|
46
|
+
onRequireUserIntegrityCheck?: ((flags: UserIntegrityCheckFlag) => void) | undefined;
|
|
36
47
|
};
|
|
37
48
|
export type ActionEventParams = {
|
|
38
49
|
event: string | string[];
|
|
@@ -5,7 +5,7 @@ export declare const generateAlias: (size?: number | undefined) => string;
|
|
|
5
5
|
/**
|
|
6
6
|
* Apply the Query to a given Knex query builder instance
|
|
7
7
|
*/
|
|
8
|
-
export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, options?: {
|
|
8
|
+
export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex.QueryBuilder, query: Query, schema: SchemaOverview, cases: Filter[], options?: {
|
|
9
9
|
aliasMap?: AliasMap;
|
|
10
10
|
isInnerQuery?: boolean;
|
|
11
11
|
hasMultiRelationalSort?: boolean | undefined;
|
|
@@ -32,10 +32,11 @@ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery:
|
|
|
32
32
|
};
|
|
33
33
|
export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
|
|
34
34
|
export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
|
|
35
|
-
export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap): {
|
|
35
|
+
export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap, cases: Filter[]): {
|
|
36
36
|
query: Knex.QueryBuilder<any, any>;
|
|
37
37
|
hasJoins: boolean;
|
|
38
38
|
hasMultiRelationalFilter: boolean;
|
|
39
39
|
};
|
|
40
|
-
export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string):
|
|
40
|
+
export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): void;
|
|
41
41
|
export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string, hasJoins: boolean): void;
|
|
42
|
+
export declare function joinFilterWithCases(filter: Filter | null | undefined, cases: Filter[]): Filter | null;
|
|
@@ -14,7 +14,7 @@ export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
|
|
|
14
14
|
/**
|
|
15
15
|
* Apply the Query to a given Knex query builder instance
|
|
16
16
|
*/
|
|
17
|
-
export default function applyQuery(knex, collection, dbQuery, query, schema, options) {
|
|
17
|
+
export default function applyQuery(knex, collection, dbQuery, query, schema, cases, options) {
|
|
18
18
|
const aliasMap = options?.aliasMap ?? Object.create(null);
|
|
19
19
|
let hasJoins = false;
|
|
20
20
|
let hasMultiRelationalFilter = false;
|
|
@@ -37,8 +37,14 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
37
37
|
if (query.group) {
|
|
38
38
|
dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// `cases` are the permissions cases that are required for the current data set. We're
|
|
41
|
+
// dynamically adding those into the filters that the user provided to enforce the permission
|
|
42
|
+
// rules. You should be able to read an item if one or more of the cases matches. The actual case
|
|
43
|
+
// is reused in the column selection case/when to dynamically return or nullify the field values
|
|
44
|
+
// you're actually allowed to read
|
|
45
|
+
const filter = joinFilterWithCases(query.filter, cases);
|
|
46
|
+
if (filter) {
|
|
47
|
+
const filterResult = applyFilter(knex, schema, dbQuery, filter, collection, aliasMap, cases);
|
|
42
48
|
if (!hasJoins) {
|
|
43
49
|
hasJoins = filterResult.hasJoins;
|
|
44
50
|
}
|
|
@@ -226,7 +232,7 @@ export function applyOffset(knex, rootQuery, offset) {
|
|
|
226
232
|
getHelpers(knex).schema.applyOffset(rootQuery, offset);
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
|
-
export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap) {
|
|
235
|
+
export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap, cases) {
|
|
230
236
|
const helpers = getHelpers(knex);
|
|
231
237
|
const relations = schema.relations;
|
|
232
238
|
let hasJoins = false;
|
|
@@ -235,12 +241,23 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
235
241
|
addWhereClauses(knex, rootQuery, rootFilter, collection);
|
|
236
242
|
return { query: rootQuery, hasJoins, hasMultiRelationalFilter };
|
|
237
243
|
function addJoins(dbQuery, filter, collection) {
|
|
238
|
-
|
|
244
|
+
// eslint-disable-next-line prefer-const
|
|
245
|
+
for (let [key, value] of Object.entries(filter)) {
|
|
239
246
|
if (key === '_or' || key === '_and') {
|
|
240
247
|
// If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
|
|
241
248
|
// permission checks, as {} already matches full permissions.
|
|
242
249
|
if (key === '_or' && value.some((subFilter) => Object.keys(subFilter).length === 0)) {
|
|
243
|
-
|
|
250
|
+
// But only do so, if the value is not equal to `cases` (since then this is not permission related at all)
|
|
251
|
+
// or the length of value is 1, ie. only the empty filter.
|
|
252
|
+
// If the length is more than one it means that some items (and fields) might now be available, so
|
|
253
|
+
// the joins are required for the case/when construction.
|
|
254
|
+
if (value !== cases || value.length === 1) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Otherwise we can at least filter out all empty filters that would not add joins anyway
|
|
259
|
+
value = value.filter((subFilter) => Object.keys(subFilter).length > 0);
|
|
260
|
+
}
|
|
244
261
|
}
|
|
245
262
|
value.forEach((subFilter) => {
|
|
246
263
|
addJoins(dbQuery, subFilter, collection);
|
|
@@ -311,7 +328,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
311
328
|
.select({ [field]: column })
|
|
312
329
|
.from(collection)
|
|
313
330
|
.whereNotNull(column);
|
|
314
|
-
applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema);
|
|
331
|
+
applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, cases);
|
|
315
332
|
};
|
|
316
333
|
const childKey = Object.keys(value)?.[0];
|
|
317
334
|
if (childKey === '_none') {
|
|
@@ -551,7 +568,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
551
568
|
}
|
|
552
569
|
}
|
|
553
570
|
}
|
|
554
|
-
export
|
|
571
|
+
export function applySearch(knex, schema, dbQuery, searchQuery, collection) {
|
|
555
572
|
const { number: numberHelper } = getHelpers(knex);
|
|
556
573
|
const fields = Object.entries(schema.collections[collection].fields);
|
|
557
574
|
dbQuery.andWhere(function () {
|
|
@@ -627,6 +644,18 @@ export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins)
|
|
|
627
644
|
}
|
|
628
645
|
}
|
|
629
646
|
}
|
|
647
|
+
export function joinFilterWithCases(filter, cases) {
|
|
648
|
+
if (cases.length > 0 && !filter) {
|
|
649
|
+
return { _or: cases };
|
|
650
|
+
}
|
|
651
|
+
else if (filter && cases.length === 0) {
|
|
652
|
+
return filter ?? null;
|
|
653
|
+
}
|
|
654
|
+
else if (filter && cases.length > 0) {
|
|
655
|
+
return { _and: [filter, { _or: cases }] };
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
630
659
|
function getFilterPath(key, value) {
|
|
631
660
|
const path = [key];
|
|
632
661
|
const childKey = Object.keys(value)[0];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
export interface AccessLookup {
|
|
4
|
+
role: string | null;
|
|
5
|
+
user: string | null;
|
|
6
|
+
app_access: boolean | number;
|
|
7
|
+
admin_access: boolean | number;
|
|
8
|
+
}
|
|
9
|
+
export interface FetchAccessLookupOptions {
|
|
10
|
+
excludeAccessRows?: PrimaryKey[];
|
|
11
|
+
excludePolicies?: PrimaryKey[];
|
|
12
|
+
excludeUsers?: PrimaryKey[];
|
|
13
|
+
excludeRoles?: PrimaryKey[];
|
|
14
|
+
adminOnly?: boolean;
|
|
15
|
+
knex: Knex;
|
|
16
|
+
}
|
|
17
|
+
export declare function fetchAccessLookup(options: FetchAccessLookupOptions): Promise<AccessLookup[]>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function fetchAccessLookup(options) {
|
|
2
|
+
let query = options.knex
|
|
3
|
+
.select('directus_access.role', 'directus_access.user', 'directus_policies.app_access', 'directus_policies.admin_access')
|
|
4
|
+
.from('directus_access')
|
|
5
|
+
.leftJoin('directus_policies', 'directus_access.policy', 'directus_policies.id');
|
|
6
|
+
if (options.excludeAccessRows && options.excludeAccessRows.length > 0) {
|
|
7
|
+
query = query.whereNotIn('directus_access.id', options.excludeAccessRows);
|
|
8
|
+
}
|
|
9
|
+
if (options.excludePolicies && options.excludePolicies.length > 0) {
|
|
10
|
+
query = query.whereNotIn('directus_access.policy', options.excludePolicies);
|
|
11
|
+
}
|
|
12
|
+
if (options.excludeUsers && options.excludeUsers.length > 0) {
|
|
13
|
+
query = query.where((q) => q.whereNotIn('directus_access.user', options.excludeUsers).orWhereNull('directus_access.user'));
|
|
14
|
+
}
|
|
15
|
+
if (options.excludeRoles && options.excludeRoles.length > 0) {
|
|
16
|
+
query = query.where((q) => q.whereNotIn('directus_access.role', options.excludeRoles).orWhereNull('directus_access.role'));
|
|
17
|
+
}
|
|
18
|
+
if (options.adminOnly) {
|
|
19
|
+
query = query.where('directus_policies.admin_access', 1);
|
|
20
|
+
}
|
|
21
|
+
return query;
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
export interface FetchAccessRolesOptions {
|
|
4
|
+
adminRoles: Set<string>;
|
|
5
|
+
appRoles: Set<string>;
|
|
6
|
+
excludeRoles?: PrimaryKey[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Return a set of roles that allow app or admin access, if itself or any of its parents do
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchAccessRoles(options: FetchAccessRolesOptions, context: {
|
|
12
|
+
knex: Knex;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
adminRoles: Set<string>;
|
|
15
|
+
appRoles: Set<string>;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return a set of roles that allow app or admin access, if itself or any of its parents do
|
|
3
|
+
*/
|
|
4
|
+
export async function fetchAccessRoles(options, context) {
|
|
5
|
+
// Only fetch the roles that have a parent, as otherwise those roles should already be included in at least one of the input set
|
|
6
|
+
const allChildRoles = await context.knex
|
|
7
|
+
.select('id', 'parent')
|
|
8
|
+
.from('directus_roles')
|
|
9
|
+
.whereNotNull('parent')
|
|
10
|
+
.whereNotIn('id', options.excludeRoles ?? []);
|
|
11
|
+
const adminRoles = new Set(options.adminRoles);
|
|
12
|
+
const appRoles = new Set(options.appRoles);
|
|
13
|
+
const remainingRoles = new Set(allChildRoles);
|
|
14
|
+
let hasChanged = remainingRoles.size > 0;
|
|
15
|
+
// This loop accounts for the undefined order in which the roles are returned, as there is the possibility
|
|
16
|
+
// of a role parent not being in the set of roles yet, so we need to iterate over the roles multiple times
|
|
17
|
+
// until no further roles are added to the sets
|
|
18
|
+
while (hasChanged) {
|
|
19
|
+
hasChanged = false;
|
|
20
|
+
for (const role of remainingRoles) {
|
|
21
|
+
if (adminRoles.has(role.parent)) {
|
|
22
|
+
adminRoles.add(role.id);
|
|
23
|
+
remainingRoles.delete(role);
|
|
24
|
+
hasChanged = true;
|
|
25
|
+
}
|
|
26
|
+
if (appRoles.has(role.parent)) {
|
|
27
|
+
appRoles.add(role.id);
|
|
28
|
+
remainingRoles.delete(role);
|
|
29
|
+
hasChanged = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
adminRoles,
|
|
35
|
+
appRoles,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FetchAccessLookupOptions } from './fetch-access-lookup.js';
|
|
2
|
+
export interface FetchUserCountOptions extends FetchAccessLookupOptions {
|
|
3
|
+
}
|
|
4
|
+
export interface UserCount {
|
|
5
|
+
admin: number;
|
|
6
|
+
app: number;
|
|
7
|
+
api: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns counts of all active users in the system grouped by admin, app, and api access
|
|
11
|
+
*/
|
|
12
|
+
export declare function fetchUserCount(options: FetchUserCountOptions): Promise<UserCount>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { toBoolean } from '@directus/utils';
|
|
2
|
+
import { fetchAccessLookup } from './fetch-access-lookup.js';
|
|
3
|
+
import { fetchAccessRoles } from './fetch-access-roles.js';
|
|
4
|
+
import { getUserCountQuery } from './get-user-count-query.js';
|
|
5
|
+
/**
|
|
6
|
+
* Returns counts of all active users in the system grouped by admin, app, and api access
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchUserCount(options) {
|
|
9
|
+
const accessRows = await fetchAccessLookup(options);
|
|
10
|
+
const adminRoles = new Set(accessRows.filter((row) => toBoolean(row.admin_access) && row.role !== null).map((row) => row.role));
|
|
11
|
+
const appRoles = new Set(accessRows
|
|
12
|
+
.filter((row) => !toBoolean(row.admin_access) && toBoolean(row.app_access) && row.role !== null)
|
|
13
|
+
.map((row) => row.role));
|
|
14
|
+
// All users that are directly granted rights through a connected policy
|
|
15
|
+
const adminUsers = new Set(accessRows.filter((row) => toBoolean(row.admin_access) && row.user !== null).map((row) => row.user));
|
|
16
|
+
// Some roles might be granted access rights through nesting, so determine all roles that grant admin or app access,
|
|
17
|
+
// including nested roles
|
|
18
|
+
const { adminRoles: allAdminRoles, appRoles: allAppRoles } = await fetchAccessRoles({
|
|
19
|
+
adminRoles,
|
|
20
|
+
appRoles,
|
|
21
|
+
...options,
|
|
22
|
+
}, { knex: options.knex });
|
|
23
|
+
// All users that are granted admin rights through a role, but not directly
|
|
24
|
+
const adminCountQuery = getUserCountQuery(options.knex, {
|
|
25
|
+
includeRoles: Array.from(allAdminRoles),
|
|
26
|
+
excludeIds: [...adminUsers, ...(options.excludeUsers ?? [])],
|
|
27
|
+
});
|
|
28
|
+
if (options.adminOnly) {
|
|
29
|
+
// Shortcut for only counting admin users
|
|
30
|
+
const adminResult = await adminCountQuery;
|
|
31
|
+
return {
|
|
32
|
+
admin: Number(adminResult?.['count'] ?? 0) + adminUsers.size,
|
|
33
|
+
app: 0,
|
|
34
|
+
api: 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const appUsers = new Set(accessRows
|
|
38
|
+
.filter((row) => !toBoolean(row.admin_access) && toBoolean(row.app_access) && row.user !== null)
|
|
39
|
+
.map((row) => row.user));
|
|
40
|
+
// All users that are granted app rights through a role, but not directly, and that aren't admin users
|
|
41
|
+
const appCountQuery = getUserCountQuery(options.knex, {
|
|
42
|
+
includeRoles: Array.from(allAppRoles),
|
|
43
|
+
excludeRoles: Array.from(allAdminRoles),
|
|
44
|
+
excludeIds: [...appUsers, ...adminUsers, ...(options.excludeUsers ?? [])],
|
|
45
|
+
});
|
|
46
|
+
const allCountQuery = getUserCountQuery(options.knex, {
|
|
47
|
+
excludeIds: options.excludeUsers ?? [],
|
|
48
|
+
});
|
|
49
|
+
const [adminResult, appResult, allResult] = await Promise.all([adminCountQuery, appCountQuery, allCountQuery]);
|
|
50
|
+
const adminCount = Number(adminResult?.['count'] ?? 0) + adminUsers.size;
|
|
51
|
+
const appCount = Number(appResult?.['count'] ?? 0) + appUsers.size;
|
|
52
|
+
return {
|
|
53
|
+
admin: adminCount,
|
|
54
|
+
app: appCount,
|
|
55
|
+
api: Number(allResult?.['count'] ?? 0) - adminCount - appCount,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PrimaryKey } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
export interface GetUserCountOptions {
|
|
4
|
+
excludeIds?: PrimaryKey[];
|
|
5
|
+
excludeRoles?: PrimaryKey[];
|
|
6
|
+
includeRoles?: PrimaryKey[];
|
|
7
|
+
}
|
|
8
|
+
export declare function getUserCountQuery(knex: Knex, options: GetUserCountOptions): Promise<{
|
|
9
|
+
count: number;
|
|
10
|
+
}> | Knex.QueryBuilder<any, {
|
|
11
|
+
_base: {};
|
|
12
|
+
_hasSelection: true;
|
|
13
|
+
_keys: never;
|
|
14
|
+
_aliases: {};
|
|
15
|
+
_single: false;
|
|
16
|
+
_intersectProps: {
|
|
17
|
+
count?: string | number;
|
|
18
|
+
};
|
|
19
|
+
_unionProps: undefined;
|
|
20
|
+
}>;
|