From dcdccf1e79ed57d76fd2ac2884b6acedd9da8f55 Mon Sep 17 00:00:00 2001 From: Artem Savchenko Date: Mon, 13 Apr 2026 15:33:04 +0700 Subject: [PATCH] Fix controlled document print Signed-off-by: Artem Savchenko --- foundations/core/packages/core/src/classes.ts | 7 +++++++ .../packages/middleware/src/spaceSecurity.ts | 20 +++++-------------- .../src/tests/spaceSecurity.test.ts | 11 +++++----- models/core/src/core.ts | 1 + models/love/src/index.ts | 3 ++- models/love/src/migration.ts | 17 +++++++++++++++- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts index d780f9cc53..1eec454297 100644 --- a/foundations/core/packages/core/src/classes.ts +++ b/foundations/core/packages/core/src/classes.ts @@ -987,6 +987,13 @@ export interface ClassCollaborators extends Doc { fields: (keyof T)[] // PersonId | Ref | PersonId[] | Ref[] provideSecurity?: boolean // If true, will provide security for collaborators provideAttachedSecurity?: boolean // If true, will provide security for collaborators of attached doc + /** + * If true, guest / read-only-guest `findAll` queries for this class are intersected with + * document ids the account has as {@link Collaborator} (middleware), in addition to DB-level + * checks when {@link provideSecurity} is enabled. Prefer this over overloading `provideSecurity` + * when only meeting-minutes-style id filtering is needed. + */ + restrictGuestReadToCollaborators?: boolean } export interface Collaborator extends AttachedDoc { diff --git a/foundations/server/packages/middleware/src/spaceSecurity.ts b/foundations/server/packages/middleware/src/spaceSecurity.ts index ff52d11a72..a276669f42 100644 --- a/foundations/server/packages/middleware/src/spaceSecurity.ts +++ b/foundations/server/packages/middleware/src/spaceSecurity.ts @@ -27,6 +27,7 @@ import core, { type FindResult, generateId, getClassCollaborators, + mergeQueries, type LookupData, type MeasureContext, type ObjQueryType, @@ -613,19 +614,6 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar return domain === 'tx' ? 'objectSpace' : domain === 'space' ? '_id' : 'space' } - private mergeDocIdRestriction(query: DocumentQuery, allowed: Ref[]): DocumentQuery { - const allowedIds: DocumentQuery['_id'] = { $in: allowed.length === 0 ? [] : allowed } - const prevId = query._id - if (prevId === undefined) { - return { ...query, _id: allowedIds } - } - type WithAnd = DocumentQuery & { $and?: DocumentQuery[] } - const { _id: _drop, $and, ...rest } = query as WithAnd - const andParts: DocumentQuery[] = [...($and ?? []), { _id: prevId }, { _id: allowedIds }] - const merged: DocumentQuery = { ...rest, $and: andParts } - return merged - } - private async applyGuestCollaboratorReadRestriction( ctx: MeasureContext, _class: Ref>, @@ -642,7 +630,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, _class) - if (collabSec?.provideSecurity !== true) { + if (collabSec?.restrictGuestReadToCollaborators !== true) { return query } @@ -658,7 +646,9 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar { projection: { attachedTo: 1 }, limit: 10_000 } )) as Collaborator[] const allowed = collabs.map((c) => c.attachedTo) as Ref[] - return this.mergeDocIdRestriction(query, allowed) + const allowedIds: DocumentQuery['_id'] = { $in: allowed.length === 0 ? [] : allowed } + const restriction: DocumentQuery = { _id: allowedIds } + return mergeQueries(query, restriction) } override async findAll( diff --git a/foundations/server/packages/middleware/src/tests/spaceSecurity.test.ts b/foundations/server/packages/middleware/src/tests/spaceSecurity.test.ts index 62e45c76ec..6ad88b4783 100644 --- a/foundations/server/packages/middleware/src/tests/spaceSecurity.test.ts +++ b/foundations/server/packages/middleware/src/tests/spaceSecurity.test.ts @@ -60,7 +60,7 @@ function makeCtx (account: Account): MeasureContext { function makeMiddleware ( role: AccountRole, - opts: { provideSecurity: boolean } = { provideSecurity: false } + opts: { restrictGuestReadToCollaborators: boolean } = { restrictGuestReadToCollaborators: false } ): { mw: SpaceSecurityMiddleware, account: Account, calls: Array<{ cls: Ref>, query: any }> } { const account = makeAccount(role) const calls: Array<{ cls: Ref>, query: any }> = [] @@ -70,7 +70,8 @@ function makeMiddleware ( space: core.space.Model, attachedTo: DOC_CLASS, fields: ['createdBy'], - provideSecurity: opts.provideSecurity, + provideSecurity: false, + restrictGuestReadToCollaborators: opts.restrictGuestReadToCollaborators, modifiedOn: Date.now(), modifiedBy: core.account.System } as unknown as ClassCollaborators @@ -144,8 +145,8 @@ function makeMiddleware ( } describe('SpaceSecurityMiddleware guest collaborator read restriction', () => { - it('applies collaborator _id filter for guest when provideSecurity is enabled', async () => { - const { mw, account, calls } = makeMiddleware(AccountRole.Guest, { provideSecurity: true }) + it('applies collaborator _id filter for guest when restrictGuestReadToCollaborators is enabled', async () => { + const { mw, account, calls } = makeMiddleware(AccountRole.Guest, { restrictGuestReadToCollaborators: true }) const ctx = makeCtx(account) await mw.findAll(ctx, DOC_CLASS, { title: 'Meeting minutes' }) @@ -160,7 +161,7 @@ describe('SpaceSecurityMiddleware guest collaborator read restriction', () => { }) it('keeps query unchanged for regular user', async () => { - const { mw, account, calls } = makeMiddleware(AccountRole.User, { provideSecurity: true }) + const { mw, account, calls } = makeMiddleware(AccountRole.User, { restrictGuestReadToCollaborators: true }) const ctx = makeCtx(account) await mw.findAll(ctx, DOC_CLASS, { title: 'Meeting minutes' }) diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 0866c91bb7..3819da1409 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -436,6 +436,7 @@ export class TClassCollaborators extends TDoc implements ClassCollaborators fields!: (keyof Doc)[] provideSecurity?: boolean provideAttachedSecurity?: boolean + restrictGuestReadToCollaborators?: boolean } @Model(core.class.Collaborator, core.class.Doc, DOMAIN_COLLABORATOR) diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 343c2aa218..7036f28e3d 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -644,7 +644,8 @@ export function createModel (builder: Builder): void { builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { attachedTo: love.class.MeetingMinutes, fields: ['createdBy'], - provideSecurity: true + provideSecurity: true, + restrictGuestReadToCollaborators: true }) builder.mixin(love.class.Room, core.class.Class, core.mixin.IndexConfiguration, { diff --git a/models/love/src/migration.ts b/models/love/src/migration.ts index ca4779c808..eea279ab78 100644 --- a/models/love/src/migration.ts +++ b/models/love/src/migration.ts @@ -14,7 +14,7 @@ // import contact from '@hcengineering/contact' -import { TxOperations, type Ref, type Space } from '@hcengineering/core' +import { DOMAIN_MODEL, TxOperations, type Ref, type Space } from '@hcengineering/core' import drive from '@hcengineering/drive' import { MeetingStatus, @@ -179,6 +179,21 @@ export const loveOperation: MigrateOperation = { func: async (client) => { await client.reindex(DOMAIN_MEETING_MINUTES, [love.class.MeetingMinutes]) } + }, + { + state: 'meeting-minutes-restrict-guest-read-to-collaborators', + mode: 'upgrade', + func: async (client: MigrationClient) => { + await client.update( + DOMAIN_MODEL, + { + _class: core.class.ClassCollaborators, + attachedTo: love.class.MeetingMinutes, + restrictGuestReadToCollaborators: { $exists: false } + }, + { restrictGuestReadToCollaborators: true } + ) + } } ]) },