@lenne.tech/nest-server 11.34.0 → 11.35.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/.claude/rules/architecture.md +3 -1
- package/.claude/rules/configurable-features.md +3 -3
- package/.claude/rules/role-system.md +281 -0
- package/CLAUDE.md +11 -1
- package/FRAMEWORK-API.md +7 -3
- package/dist/core/common/decorators/nested-type.registry.d.ts +2 -0
- package/dist/core/common/decorators/nested-type.registry.js +17 -0
- package/dist/core/common/decorators/nested-type.registry.js.map +1 -0
- package/dist/core/common/decorators/restricted.decorator.d.ts +2 -1
- package/dist/core/common/decorators/restricted.decorator.js +52 -16
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/decorators/restrictions-checked.marker.d.ts +4 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js +30 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js.map +1 -0
- package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -1
- package/dist/core/common/decorators/unified-field.decorator.js +6 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/enums/role.enum.d.ts +8 -0
- package/dist/core/common/enums/role.enum.js +28 -1
- package/dist/core/common/enums/role.enum.js.map +1 -1
- package/dist/core/common/helpers/execution-context-request.helper.d.ts +7 -0
- package/dist/core/common/helpers/execution-context-request.helper.js +31 -0
- package/dist/core/common/helpers/execution-context-request.helper.js.map +1 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +15 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js +96 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -0
- package/dist/core/common/helpers/gridfs.helper.js +1 -1
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/helpers/validation-message.helper.d.ts +3 -0
- package/dist/core/common/helpers/validation-message.helper.js +41 -0
- package/dist/core/common/helpers/validation-message.helper.js.map +1 -0
- package/dist/core/common/interceptors/check-security.interceptor.js +2 -1
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interceptors/response-model.interceptor.js +3 -2
- package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
- package/dist/core/common/pipes/map-and-validate.pipe.js +16 -6
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/core/common/plugins/mongoose-system-role.plugin.d.ts +1 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js +106 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-tenant.plugin.js +134 -9
- package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
- package/dist/core/common/services/core-s3.service.js +4 -1
- package/dist/core/common/services/core-s3.service.js.map +1 -1
- package/dist/core/common/services/core-tenant-context.registry.d.ts +10 -0
- package/dist/core/common/services/core-tenant-context.registry.js +12 -0
- package/dist/core/common/services/core-tenant-context.registry.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +2 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -10
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +3 -1
- package/dist/core/modules/file/core-file.service.js +62 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-access.helper.d.ts +18 -0
- package/dist/core/modules/file/file-access.helper.js +46 -0
- package/dist/core/modules/file/file-access.helper.js.map +1 -0
- package/dist/core/modules/file/file-metadata.helper.js +1 -1
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -1
- package/dist/core/modules/file/file-roles.config.d.ts +10 -0
- package/dist/core/modules/file/file-roles.config.js +78 -0
- package/dist/core/modules/file/file-roles.config.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +2 -2
- package/dist/core/modules/file/file-roles.helper.js +8 -20
- package/dist/core/modules/file/file-roles.helper.js.map +1 -1
- package/dist/core/modules/tenant/core-role-scope.registry.d.ts +28 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js +93 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +5 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +37 -11
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +9 -1
- package/dist/core/modules/tenant/core-tenant.helpers.js +65 -4
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +5 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.service.js +25 -0
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.service.js +34 -4
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/index.d.ts +1 -0
- package/dist/core/modules/tus/index.js +1 -0
- package/dist/core/modules/tus/index.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +2 -1
- package/dist/core/modules/tus/tus.constants.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +5 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/inputs/core-user.input.js +8 -0
- package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
- package/dist/core.module.js +10 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/file/file.service.js +4 -1
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +26 -6
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +60 -1
- package/migration-guides/11.34.0-to-11.34.1.md +132 -0
- package/migration-guides/11.34.x-to-11.35.x.md +832 -0
- package/package.json +1 -1
- package/src/core/common/decorators/nested-type.registry.ts +42 -0
- package/src/core/common/decorators/restricted.decorator.ts +149 -21
- package/src/core/common/decorators/restrictions-checked.marker.ts +87 -0
- package/src/core/common/decorators/unified-field.decorator.ts +4 -3
- package/src/core/common/enums/role.enum.ts +116 -0
- package/src/core/common/helpers/execution-context-request.helper.ts +102 -0
- package/src/core/common/helpers/graphql-ws-context.helper.ts +192 -0
- package/src/core/common/helpers/gridfs.helper.ts +18 -2
- package/src/core/common/helpers/validation-message.helper.ts +83 -0
- package/src/core/common/interceptors/check-security.interceptor.ts +4 -1
- package/src/core/common/interceptors/response-model.interceptor.ts +3 -2
- package/src/core/common/interfaces/server-options.interface.ts +139 -7
- package/src/core/common/pipes/map-and-validate.pipe.ts +25 -7
- package/src/core/common/plugins/mongoose-system-role.plugin.ts +193 -0
- package/src/core/common/plugins/mongoose-tenant.plugin.ts +244 -12
- package/src/core/common/services/core-s3.service.ts +9 -1
- package/src/core/common/services/core-tenant-context.registry.ts +50 -0
- package/src/core/modules/auth/guards/roles.guard.ts +7 -5
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +7 -13
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +95 -1
- package/src/core/modules/file/README.md +61 -0
- package/src/core/modules/file/core-file.service.ts +166 -9
- package/src/core/modules/file/file-access.helper.ts +149 -0
- package/src/core/modules/file/file-metadata.helper.ts +13 -5
- package/src/core/modules/file/file-roles.config.ts +202 -0
- package/src/core/modules/file/file-roles.helper.ts +11 -40
- package/src/core/modules/tenant/README.md +20 -1
- package/src/core/modules/tenant/core-role-scope.registry.ts +195 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +92 -12
- package/src/core/modules/tenant/core-tenant.helpers.ts +139 -7
- package/src/core/modules/tenant/core-tenant.module.ts +12 -0
- package/src/core/modules/tenant/core-tenant.service.ts +56 -0
- package/src/core/modules/tus/README.md +46 -7
- package/src/core/modules/tus/core-tus.service.ts +94 -5
- package/src/core/modules/tus/index.ts +1 -0
- package/src/core/modules/tus/tus.constants.ts +17 -1
- package/src/core/modules/user/core-user.service.ts +12 -0
- package/src/core/modules/user/inputs/core-user.input.ts +28 -2
- package/src/core.module.ts +62 -1
- package/src/index.ts +3 -0
- package/src/server/modules/file/file.service.ts +45 -2
- package/src/test/test.helper.ts +54 -11
package/src/core.module.ts
CHANGED
|
@@ -8,6 +8,10 @@ import graphqlUploadExpress = require('graphql-upload/graphqlUploadExpress.js');
|
|
|
8
8
|
import mongoose from 'mongoose';
|
|
9
9
|
|
|
10
10
|
import { merge } from './core/common/helpers/config.helper';
|
|
11
|
+
import {
|
|
12
|
+
buildRequestContextAwareExecute,
|
|
13
|
+
buildRequestContextAwareSubscribe,
|
|
14
|
+
} from './core/common/helpers/graphql-ws-context.helper';
|
|
11
15
|
import { CheckResponseInterceptor } from './core/common/interceptors/check-response.interceptor';
|
|
12
16
|
import { CheckSecurityInterceptor } from './core/common/interceptors/check-security.interceptor';
|
|
13
17
|
import { ResponseModelInterceptor } from './core/common/interceptors/response-model.interceptor';
|
|
@@ -31,6 +35,7 @@ import { mongooseIdPlugin } from './core/common/plugins/mongoose-id.plugin';
|
|
|
31
35
|
import { mongooseAuditFieldsPlugin } from './core/common/plugins/mongoose-audit-fields.plugin';
|
|
32
36
|
import { mongoosePasswordPlugin } from './core/common/plugins/mongoose-password.plugin';
|
|
33
37
|
import { mongooseRoleGuardPlugin } from './core/common/plugins/mongoose-role-guard.plugin';
|
|
38
|
+
import { mongooseSystemRolePlugin } from './core/common/plugins/mongoose-system-role.plugin';
|
|
34
39
|
import { mongooseTenantPlugin } from './core/common/plugins/mongoose-tenant.plugin';
|
|
35
40
|
import { ConfigService } from './core/common/services/config.service';
|
|
36
41
|
import { CoreCronJobsInitializer } from './core/common/services/core-cron-jobs.initializer';
|
|
@@ -46,7 +51,7 @@ import { CoreBetterAuthUserMapper } from './core/modules/better-auth/core-better
|
|
|
46
51
|
import { CoreBetterAuthModule } from './core/modules/better-auth/core-better-auth.module';
|
|
47
52
|
import { CoreBetterAuthService } from './core/modules/better-auth/core-better-auth.service';
|
|
48
53
|
import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
|
|
49
|
-
import { applyFileRoles } from './core/modules/file/file-roles.helper';
|
|
54
|
+
import { applyFileRoles, warnOnPresignedDownloadsWithRestrictedRoles } from './core/modules/file/file-roles.helper';
|
|
50
55
|
import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
|
|
51
56
|
import { CoreHubModule } from './core/modules/hub/core-hub.module';
|
|
52
57
|
import { isHubEnabled, isHubQueriesEnabled } from './core/modules/hub/hub-config.helper';
|
|
@@ -253,6 +258,12 @@ export class CoreModule implements NestModule {
|
|
|
253
258
|
if (config.security?.mongooseRoleGuardPlugin !== false) {
|
|
254
259
|
connection.plugin(mongooseRoleGuardPlugin);
|
|
255
260
|
}
|
|
261
|
+
// Refuse to store system roles (s_*) in a roles array. Deliberately NOT configurable and
|
|
262
|
+
// registered independently of the role guard above: that one decides WHO may change roles
|
|
263
|
+
// (and can be switched off), this one decides WHICH values may exist at all. A stored
|
|
264
|
+
// 's_self' satisfies every S_SELF check on arbitrary users, so there is no configuration
|
|
265
|
+
// under which writing one is legitimate.
|
|
266
|
+
connection.plugin(mongooseSystemRolePlugin);
|
|
256
267
|
// Add audit fields plugin (enabled by default, opt-out via config)
|
|
257
268
|
if (config.security?.mongooseAuditFieldsPlugin !== false) {
|
|
258
269
|
connection.plugin(mongooseAuditFieldsPlugin);
|
|
@@ -273,6 +284,11 @@ export class CoreModule implements NestModule {
|
|
|
273
284
|
// function and opt out of this; see applyFileRoles() for why.
|
|
274
285
|
applyFileRoles(config.file);
|
|
275
286
|
|
|
287
|
+
// Presigned downloads hand out a bearer URL that ignores roles after it is issued, so pairing
|
|
288
|
+
// them with a restricted downloadRoles is usually an oversight. Warn rather than fail — the
|
|
289
|
+
// combination has legitimate uses, it just should not be reached by accident.
|
|
290
|
+
warnOnPresignedDownloadsWithRestrictedRoles(config.s3, config.file);
|
|
291
|
+
|
|
276
292
|
// Check secrets
|
|
277
293
|
const jwtConfig = config.jwt;
|
|
278
294
|
if (jwtConfig?.secret && jwtConfig.secret && jwtConfig.refresh && jwtConfig.refresh.secret === jwtConfig.secret) {
|
|
@@ -593,6 +609,21 @@ export class CoreModule implements NestModule {
|
|
|
593
609
|
cors,
|
|
594
610
|
installSubscriptionHandlers: true,
|
|
595
611
|
subscriptions: {
|
|
612
|
+
// WEBSOCKET REQUEST CONTEXT. `GqlSubscriptionService` destructures `execute` /
|
|
613
|
+
// `subscribe` from its own options and hands them to BOTH WS transports, so these two
|
|
614
|
+
// are the only place where a whole WS operation is reachable as one call. They must sit
|
|
615
|
+
// INSIDE `subscriptions`: `ApolloDriver.start()` forwards only
|
|
616
|
+
// `{ schema, path, context, ...options.subscriptions }`, so a top-level pair is
|
|
617
|
+
// silently dropped.
|
|
618
|
+
//
|
|
619
|
+
// Without them a WS operation runs with NO `RequestContext` at all — no Express
|
|
620
|
+
// middleware runs on an upgrade, and `CoreTenantGuard.getRequest()` finds no `req` on a
|
|
621
|
+
// subscription context — and `mongooseTenantPlugin` reads "no context" as "system
|
|
622
|
+
// operation, no filter". A tenant-scoped read while delivering a subscription message
|
|
623
|
+
// therefore returned EVERY tenant's rows, with the plugin's safety net unable to
|
|
624
|
+
// notice. HTTP does not pass through these (Apollo runs its own pipeline).
|
|
625
|
+
execute: buildRequestContextAwareExecute(),
|
|
626
|
+
subscribe: buildRequestContextAwareSubscribe(),
|
|
596
627
|
'graphql-ws': {
|
|
597
628
|
context: ({ extra }) => extra,
|
|
598
629
|
onConnect: async (context: Context<any, any>) => {
|
|
@@ -688,6 +719,21 @@ export class CoreModule implements NestModule {
|
|
|
688
719
|
cors,
|
|
689
720
|
installSubscriptionHandlers: true,
|
|
690
721
|
subscriptions: {
|
|
722
|
+
// WEBSOCKET REQUEST CONTEXT. `GqlSubscriptionService` destructures `execute` /
|
|
723
|
+
// `subscribe` from its own options and hands them to BOTH WS transports, so these two
|
|
724
|
+
// are the only place where a whole WS operation is reachable as one call. They must sit
|
|
725
|
+
// INSIDE `subscriptions`: `ApolloDriver.start()` forwards only
|
|
726
|
+
// `{ schema, path, context, ...options.subscriptions }`, so a top-level pair is
|
|
727
|
+
// silently dropped.
|
|
728
|
+
//
|
|
729
|
+
// Without them a WS operation runs with NO `RequestContext` at all — no Express
|
|
730
|
+
// middleware runs on an upgrade, and `CoreTenantGuard.getRequest()` finds no `req` on a
|
|
731
|
+
// subscription context — and `mongooseTenantPlugin` reads "no context" as "system
|
|
732
|
+
// operation, no filter". A tenant-scoped read while delivering a subscription message
|
|
733
|
+
// therefore returned EVERY tenant's rows, with the plugin's safety net unable to
|
|
734
|
+
// notice. HTTP does not pass through these (Apollo runs its own pipeline).
|
|
735
|
+
execute: buildRequestContextAwareExecute(),
|
|
736
|
+
subscribe: buildRequestContextAwareSubscribe(),
|
|
691
737
|
'graphql-ws': {
|
|
692
738
|
context: ({ extra }) => extra,
|
|
693
739
|
onConnect: async (context: Context<any, any>) => {
|
|
@@ -800,6 +846,21 @@ export class CoreModule implements NestModule {
|
|
|
800
846
|
cors,
|
|
801
847
|
installSubscriptionHandlers: true,
|
|
802
848
|
subscriptions: {
|
|
849
|
+
// WEBSOCKET REQUEST CONTEXT. `GqlSubscriptionService` destructures `execute` /
|
|
850
|
+
// `subscribe` from its own options and hands them to BOTH WS transports, so these two
|
|
851
|
+
// are the only place where a whole WS operation is reachable as one call. They must sit
|
|
852
|
+
// INSIDE `subscriptions`: `ApolloDriver.start()` forwards only
|
|
853
|
+
// `{ schema, path, context, ...options.subscriptions }`, so a top-level pair is
|
|
854
|
+
// silently dropped.
|
|
855
|
+
//
|
|
856
|
+
// Without them a WS operation runs with NO `RequestContext` at all — no Express
|
|
857
|
+
// middleware runs on an upgrade, and `CoreTenantGuard.getRequest()` finds no `req` on a
|
|
858
|
+
// subscription context — and `mongooseTenantPlugin` reads "no context" as "system
|
|
859
|
+
// operation, no filter". A tenant-scoped read while delivering a subscription message
|
|
860
|
+
// therefore returned EVERY tenant's rows, with the plugin's safety net unable to
|
|
861
|
+
// notice. HTTP does not pass through these (Apollo runs its own pipeline).
|
|
862
|
+
execute: buildRequestContextAwareExecute(),
|
|
863
|
+
subscribe: buildRequestContextAwareSubscribe(),
|
|
803
864
|
'graphql-ws': {
|
|
804
865
|
context: ({ extra }) => extra,
|
|
805
866
|
onConnect: async (context: Context<any, any>) => {
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ export * from './core/common/plugins/mongoose-id.plugin';
|
|
|
81
81
|
export * from './core/common/plugins/mongoose-audit-fields.plugin';
|
|
82
82
|
export * from './core/common/plugins/mongoose-password.plugin';
|
|
83
83
|
export * from './core/common/plugins/mongoose-role-guard.plugin';
|
|
84
|
+
export * from './core/common/plugins/mongoose-system-role.plugin';
|
|
84
85
|
export * from './core/common/plugins/mongoose-tenant.plugin';
|
|
85
86
|
export * from './core/common/scalars/any.scalar';
|
|
86
87
|
export * from './core/common/scalars/date-timestamp.scalar';
|
|
@@ -176,6 +177,7 @@ export * from './core/modules/file/core-file-info.model';
|
|
|
176
177
|
export * from './core/modules/file/core-file.controller';
|
|
177
178
|
export * from './core/modules/file/core-file.resolver';
|
|
178
179
|
export * from './core/modules/file/core-file.service';
|
|
180
|
+
export * from './core/modules/file/file-access.helper';
|
|
179
181
|
export * from './core/modules/file/file-roles.helper';
|
|
180
182
|
export * from './core/modules/file/file-storage.helper';
|
|
181
183
|
export * from './core/modules/file/filesystem-file.helper';
|
|
@@ -236,6 +238,7 @@ export * from './core/modules/tenant/core-tenant-member.model';
|
|
|
236
238
|
export * from './core/modules/tenant/core-tenant.decorators';
|
|
237
239
|
export * from './core/modules/tenant/core-tenant.enums';
|
|
238
240
|
export * from './core/modules/tenant/core-tenant.guard';
|
|
241
|
+
export * from './core/modules/tenant/core-role-scope.registry';
|
|
239
242
|
export * from './core/modules/tenant/core-tenant.helpers';
|
|
240
243
|
export * from './core/modules/tenant/core-tenant.module';
|
|
241
244
|
export * from './core/modules/tenant/core-tenant.service';
|
|
@@ -98,12 +98,28 @@ export class FileService extends CoreFileService {
|
|
|
98
98
|
* See `src/core/modules/file/README.md` § Access control, and
|
|
99
99
|
* `tests/file-ownership.e2e-spec.ts` for the end-to-end contract test.
|
|
100
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* NOTE FOR CONSUMERS: since 11.35.0 you may not need any of this.
|
|
103
|
+
*
|
|
104
|
+
* `file.access: 'owner'` is exactly the rule below, shipped by the framework — including the parts
|
|
105
|
+
* that are easy to get wrong (fail closed without a user, require the owner field to be PRESENT,
|
|
106
|
+
* cover the by-name branch, refuse a listing) and including the metadata stamping, which this project
|
|
107
|
+
* does by hand in `AvatarController`. `'tenant'` is the same rule against
|
|
108
|
+
* `metadata.tenantId` and the validated `RequestContext` tenant.
|
|
109
|
+
*
|
|
110
|
+
* This override stays because the reference server has to EXERCISE the seam — a rule that lives only
|
|
111
|
+
* in a preset proves the preset works, never that the inheritance point a consuming project extends
|
|
112
|
+
* still does. Keep it here; in your own project, prefer the preset unless your rights are something
|
|
113
|
+
* the framework cannot guess (an explicit read right, a case assignment, a published flag).
|
|
114
|
+
*/
|
|
101
115
|
protected override async checkRights(
|
|
102
116
|
input: any,
|
|
103
117
|
options?: FileServiceOptions & { checkInputType: FileInputCheckType },
|
|
104
118
|
): Promise<boolean> {
|
|
105
|
-
//
|
|
106
|
-
|
|
119
|
+
// Forced (system) calls and WRITES stay on the coarse role gate. Writes deliberately so: an
|
|
120
|
+
// upload has no owner to compare against yet — `AvatarController` records `metadata.ownerId` as
|
|
121
|
+
// it writes — so the only meaningful gate there is `file.uploadRoles`.
|
|
122
|
+
if (options?.force || options?.checkInputType === 'file' || options?.checkInputType === 'files') {
|
|
107
123
|
return true;
|
|
108
124
|
}
|
|
109
125
|
|
|
@@ -111,6 +127,33 @@ export class FileService extends CoreFileService {
|
|
|
111
127
|
return true;
|
|
112
128
|
}
|
|
113
129
|
|
|
130
|
+
// A LISTING cannot be narrowed by this hook, so it is refused rather than waved through.
|
|
131
|
+
//
|
|
132
|
+
// The hook is asked ONCE for the whole query, not once per row, so there is no answer here that
|
|
133
|
+
// means "…but only their own files". Returning `true` — which this rule used to do, and which is
|
|
134
|
+
// therefore the shape projects copied — hands a non-admin a full inventory of every upload the
|
|
135
|
+
// moment a project surfaces `findFileInfo()`: `CoreFileInfo` carries `filename`, `length`,
|
|
136
|
+
// `uploadDate` and the `id`, and for medical data the filename frequently IS the content.
|
|
137
|
+
//
|
|
138
|
+
// A project that wants "my files" FORCES the constraint server-side and passes `force: true`:
|
|
139
|
+
//
|
|
140
|
+
// this.fileService.findFileInfo(
|
|
141
|
+
// { filterQuery: { 'metadata.ownerId': String(currentUser.id) } },
|
|
142
|
+
// { force: true },
|
|
143
|
+
// );
|
|
144
|
+
//
|
|
145
|
+
// Note what that is NOT: it does not inspect the caller's `filterArgs` to check whether they are
|
|
146
|
+
// already narrowed. `filterArgs` is CLIENT-CONTROLLED, so approving a filter shape means
|
|
147
|
+
// validating attacker input — and any such check is one filter shape away from being wrong.
|
|
148
|
+
// Override the filter; never approve it. See tests/file-ownership.e2e-spec.ts.
|
|
149
|
+
// Explicit, even though falling through would ALSO deny: with `checkInputType: 'filterArgs'` the
|
|
150
|
+
// `input` is a FilterArgs object, so the by-name raw lookup below answers null and the comparison
|
|
151
|
+
// fails. That is an accident of the fall-through, not a decision — it would flip the moment a
|
|
152
|
+
// project's own raw lookup behaved differently for a non-string input. Say it outright instead.
|
|
153
|
+
if (options.checkInputType === 'filterArgs') {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
114
157
|
// The RAW document on purpose: the public getFileInfo() runs prepareOutput(), which
|
|
115
158
|
// strips `metadata` — the very field this decision rests on.
|
|
116
159
|
const raw =
|
package/src/test/test.helper.ts
CHANGED
|
@@ -92,6 +92,16 @@ export interface TestGraphQLOptions {
|
|
|
92
92
|
*/
|
|
93
93
|
cookies?: Record<string, string> | string;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Additional `connectionParams` for a SUBSCRIPTION handshake.
|
|
97
|
+
*
|
|
98
|
+
* Merged on top of the `Authorization` entry derived from `token`. A WebSocket carries no HTTP
|
|
99
|
+
* headers per operation, so anything the server reads from a header on the HTTP path — the tenant
|
|
100
|
+
* header above all — has to travel here instead. Without this a tenant-scoped subscription could
|
|
101
|
+
* not be tested at all.
|
|
102
|
+
*/
|
|
103
|
+
connectionParams?: Record<string, string>;
|
|
104
|
+
|
|
95
105
|
/**
|
|
96
106
|
* Count of subscription messages, specifies how many messages are to be received on subscription
|
|
97
107
|
*/
|
|
@@ -788,10 +798,13 @@ export class TestHelper {
|
|
|
788
798
|
}
|
|
789
799
|
|
|
790
800
|
// Prepare subscription
|
|
791
|
-
let connectionParams;
|
|
801
|
+
let connectionParams: Record<string, string> | undefined;
|
|
792
802
|
if (options?.token) {
|
|
793
803
|
connectionParams = { Authorization: `Bearer ${options?.token}` };
|
|
794
804
|
}
|
|
805
|
+
if (options?.connectionParams) {
|
|
806
|
+
connectionParams = { ...connectionParams, ...options.connectionParams };
|
|
807
|
+
}
|
|
795
808
|
|
|
796
809
|
// Init client
|
|
797
810
|
if (options.log) {
|
|
@@ -800,10 +813,27 @@ export class TestHelper {
|
|
|
800
813
|
const client = createClient({ connectionParams, url: this.subscriptionUrl, webSocketImpl: ws });
|
|
801
814
|
const messages: any[] = [];
|
|
802
815
|
let unsubscribe: () => void;
|
|
816
|
+
let rejectSubscription: (reason: unknown) => void;
|
|
817
|
+
|
|
803
818
|
const onNext = (message) => {
|
|
804
819
|
if (options.log) {
|
|
805
820
|
console.info('Subscription message', JSON.stringify(message, null, 2));
|
|
806
821
|
}
|
|
822
|
+
|
|
823
|
+
// A REFUSED subscription arrives as a `next` message carrying `errors`, not through the
|
|
824
|
+
// transport's `error` callback: when a guard throws, graphql's `subscribe()` returns an
|
|
825
|
+
// ExecutionResult instead of an async iterable, and graphql-ws emits that as one payload
|
|
826
|
+
// followed by `complete`. Projecting only `data[name]` therefore turned "you may not subscribe
|
|
827
|
+
// to this" into an indistinguishable `undefined`/`null` message — so a test asserting that an
|
|
828
|
+
// unauthorized subscriber gets nothing passed just as well when the gate was wide open.
|
|
829
|
+
// Rejecting makes the refusal observable, which is what the caller actually experiences.
|
|
830
|
+
if (message?.errors?.length) {
|
|
831
|
+
const detail = message.errors.map((error: any) => error?.message ?? String(error)).join('; ');
|
|
832
|
+
rejectSubscription?.(new Error(`Subscription "${graphql.name}" failed: ${detail}`));
|
|
833
|
+
unsubscribe?.();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
807
837
|
messages.push(message?.data?.[graphql.name]);
|
|
808
838
|
if (messages.length <= options.countOfSubscriptionMessages) {
|
|
809
839
|
unsubscribe();
|
|
@@ -811,16 +841,29 @@ export class TestHelper {
|
|
|
811
841
|
};
|
|
812
842
|
|
|
813
843
|
// Subscribe
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
844
|
+
//
|
|
845
|
+
// Disposed in `finally`, including on the refusal path: without it the client keeps its socket
|
|
846
|
+
// (and graphql-ws its reconnect timer) open past the end of the test, which surfaces later as a
|
|
847
|
+
// hanging process or as an error attributed to whichever test happened to run next.
|
|
848
|
+
try {
|
|
849
|
+
await new Promise((resolve, reject) => {
|
|
850
|
+
rejectSubscription = reject;
|
|
851
|
+
unsubscribe = client.subscribe(
|
|
852
|
+
{ query },
|
|
853
|
+
{
|
|
854
|
+
complete: resolve as any,
|
|
855
|
+
error: reject,
|
|
856
|
+
next: onNext,
|
|
857
|
+
},
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
} finally {
|
|
861
|
+
try {
|
|
862
|
+
await client.dispose?.();
|
|
863
|
+
} catch {
|
|
864
|
+
// Already closing — nothing to clean up, and a teardown error must not mask the result.
|
|
865
|
+
}
|
|
866
|
+
}
|
|
824
867
|
|
|
825
868
|
// Return subscribed messages
|
|
826
869
|
return messages;
|