Fix controlled document print

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artem Savchenko
2026-04-13 15:33:04 +07:00
parent ac1553bd38
commit dcdccf1e79
6 changed files with 37 additions and 22 deletions
@@ -987,6 +987,13 @@ export interface ClassCollaborators<T extends Doc> extends Doc {
fields: (keyof T)[] // PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]
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 {
@@ -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<T extends Doc>(query: DocumentQuery<T>, allowed: Ref<T>[]): DocumentQuery<T> {
const allowedIds: DocumentQuery<T>['_id'] = { $in: allowed.length === 0 ? [] : allowed }
const prevId = query._id
if (prevId === undefined) {
return { ...query, _id: allowedIds }
}
type WithAnd = DocumentQuery<T> & { $and?: DocumentQuery<T>[] }
const { _id: _drop, $and, ...rest } = query as WithAnd
const andParts: DocumentQuery<T>[] = [...($and ?? []), { _id: prevId }, { _id: allowedIds }]
const merged: DocumentQuery<T> = { ...rest, $and: andParts }
return merged
}
private async applyGuestCollaboratorReadRestriction<T extends Doc>(
ctx: MeasureContext<SessionData>,
_class: Ref<Class<T>>,
@@ -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<T>[]
return this.mergeDocIdRestriction(query, allowed)
const allowedIds: DocumentQuery<T>['_id'] = { $in: allowed.length === 0 ? [] : allowed }
const restriction: DocumentQuery<T> = { _id: allowedIds }
return mergeQueries(query, restriction)
}
override async findAll<T extends Doc>(
@@ -60,7 +60,7 @@ function makeCtx (account: Account): MeasureContext<SessionData> {
function makeMiddleware (
role: AccountRole,
opts: { provideSecurity: boolean } = { provideSecurity: false }
opts: { restrictGuestReadToCollaborators: boolean } = { restrictGuestReadToCollaborators: false }
): { mw: SpaceSecurityMiddleware, account: Account, calls: Array<{ cls: Ref<Class<Doc>>, query: any }> } {
const account = makeAccount(role)
const calls: Array<{ cls: Ref<Class<Doc>>, 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<Doc>
@@ -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' })
+1
View File
@@ -436,6 +436,7 @@ export class TClassCollaborators extends TDoc implements ClassCollaborators<Doc>
fields!: (keyof Doc)[]
provideSecurity?: boolean
provideAttachedSecurity?: boolean
restrictGuestReadToCollaborators?: boolean
}
@Model(core.class.Collaborator, core.class.Doc, DOMAIN_COLLABORATOR)
+2 -1
View File
@@ -644,7 +644,8 @@ export function createModel (builder: Builder): void {
builder.createDoc<ClassCollaborators<MeetingMinutes>>(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, {
+16 -1
View File
@@ -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 }
)
}
}
])
},