From 190b1078b5c2b61f60228474b606a5a9ec5efd85 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Tue, 16 May 2023 13:05:47 +0600 Subject: [PATCH] Refresh query security (#3189) Signed-off-by: Denis Bykhov --- packages/core/src/tx.ts | 3 +- packages/query/src/index.ts | 41 +++++++++-- .../components/issues/IssueStatusIcon.svelte | 5 +- server/core/src/pipeline.ts | 19 +++-- server/core/src/types.ts | 12 +++- server/middleware/src/configuration.ts | 3 +- server/middleware/src/modified.ts | 9 ++- server/middleware/src/private.ts | 9 ++- server/middleware/src/queryJoin.ts | 9 ++- server/middleware/src/spaceSecurity.ts | 69 ++++++++++++++++--- server/ws/src/server.ts | 11 ++- server/ws/src/types.ts | 6 +- 12 files changed, 158 insertions(+), 38 deletions(-) diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 6114320274..26ca038095 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -49,7 +49,8 @@ export interface Tx extends Doc { export enum WorkspaceEvent { UpgradeScheduled, Upgrade, - IndexingUpdate + IndexingUpdate, + SecurityChange } /** diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index cdee0197c3..8fb3b4ac42 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -97,7 +97,7 @@ export class LiveQuery extends TxProcessor implements Client { // Perform refresh of content since connection established. async refreshConnect (): Promise { for (const q of [...this.queue]) { - if (!(await this.removeFromQueue(q))) { + if (!this.removeFromQueue(q)) { try { await this.refresh(q) } catch (err) { @@ -622,9 +622,11 @@ export class LiveQuery extends TxProcessor implements Client { private async refresh (q: Query): Promise { const res = await this.client.findAll(q._class, q.query, q.options) - q.result = res - q.total = res.total - await this.callback(q) + if (!deepEqual(res, q.result)) { + q.result = res + q.total = res.total + await this.callback(q) + } } // Check if query is partially matched. @@ -974,6 +976,7 @@ export class LiveQuery extends TxProcessor implements Client { async tx (tx: Tx): Promise { if (tx._class === core.class.TxWorkspaceEvent) { await this.checkUpdateFulltextQueries(tx) + await this.changePrivateHandler(tx) return {} } return await super.tx(tx) @@ -985,7 +988,7 @@ export class LiveQuery extends TxProcessor implements Client { const indexingParam = evt.params as IndexingUpdateEvent for (const q of [...this.queue]) { if (indexingParam._class.includes(q._class) && q.query.$search !== undefined) { - if (!(await this.removeFromQueue(q))) { + if (!this.removeFromQueue(q)) { try { await this.refresh(q) } catch (err) { @@ -1008,6 +1011,34 @@ export class LiveQuery extends TxProcessor implements Client { } } + private async changePrivateHandler (tx: Tx): Promise { + const evt = tx as TxWorkspaceEvent + if (evt.event === WorkspaceEvent.SecurityChange) { + for (const q of [...this.queue]) { + if (typeof q.query.space !== 'string') { + if (!this.removeFromQueue(q)) { + try { + await this.refresh(q) + } catch (err) { + console.error(err) + } + } + } + } + for (const v of this.queries.values()) { + for (const q of v) { + if (typeof q.query.space !== 'string') { + try { + await this.refresh(q) + } catch (err) { + console.error(err) + } + } + } + } + } + } + private async __updateLookup (q: Query, updatedDoc: WithLookup, ops: any): Promise { for (const key in ops) { if (!key.startsWith('$')) { diff --git a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte index fdd3e36893..b8fd8c6be3 100644 --- a/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte +++ b/plugins/tracker-resources/src/components/issues/IssueStatusIcon.svelte @@ -40,7 +40,10 @@ $: if (value.category === tracker.issueStatusCategory.Started) { const _s = [ ...$statusStore.filter( - (it) => it.ofAttribute === value.ofAttribute && it.category === tracker.issueStatusCategory.Started + (it) => + it.ofAttribute === value.ofAttribute && + it.category === tracker.issueStatusCategory.Started && + it.space === value.space ) ] _s.sort((a, b) => a.rank.localeCompare(b.rank)) diff --git a/server/core/src/pipeline.ts b/server/core/src/pipeline.ts index 1cf4fda5fc..5790765b3d 100644 --- a/server/core/src/pipeline.ts +++ b/server/core/src/pipeline.ts @@ -29,7 +29,7 @@ import { TxResult } from '@hcengineering/core' import { DbConfiguration, createServerStorage } from './storage' -import { Middleware, MiddlewareCreator, Pipeline, SessionContext } from './types' +import { BroadcastFunc, Middleware, MiddlewareCreator, Pipeline, SessionContext } from './types' /** * @public @@ -39,13 +39,13 @@ export async function createPipeline ( conf: DbConfiguration, constructors: MiddlewareCreator[], upgrade: boolean, - broadcast: (tx: Tx[]) => void + broadcast: BroadcastFunc ): Promise { const storage = await createServerStorage(conf, { upgrade, broadcast }) - const pipeline = PipelineImpl.create(ctx, storage, constructors) + const pipeline = PipelineImpl.create(ctx, storage, constructors, broadcast) return await pipeline } @@ -59,18 +59,23 @@ class PipelineImpl implements Pipeline { static async create ( ctx: MeasureContext, storage: ServerStorage, - constructors: MiddlewareCreator[] + constructors: MiddlewareCreator[], + broadcast: BroadcastFunc ): Promise { const pipeline = new PipelineImpl(storage) - pipeline.head = await pipeline.buildChain(ctx, constructors) + pipeline.head = await pipeline.buildChain(ctx, constructors, broadcast) return pipeline } - private async buildChain (ctx: MeasureContext, constructors: MiddlewareCreator[]): Promise { + private async buildChain ( + ctx: MeasureContext, + constructors: MiddlewareCreator[], + broadcast: BroadcastFunc + ): Promise { let current: Middleware | undefined for (let index = constructors.length - 1; index >= 0; index--) { const element = constructors[index] - current = await element(ctx, this.storage, current) + current = await element(ctx, broadcast, this.storage, current) } return current } diff --git a/server/core/src/types.ts b/server/core/src/types.ts index 73f0bd21ca..12eb448a6b 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -64,7 +64,17 @@ export interface Middleware { /** * @public */ -export type MiddlewareCreator = (ctx: MeasureContext, storage: ServerStorage, next?: Middleware) => Promise +export type BroadcastFunc = (tx: Tx[], targets?: string[]) => void + +/** + * @public + */ +export type MiddlewareCreator = ( + ctx: MeasureContext, + broadcast: BroadcastFunc, + storage: ServerStorage, + next?: Middleware +) => Promise /** * @public diff --git a/server/middleware/src/configuration.ts b/server/middleware/src/configuration.ts index 35f1ca4b92..4fb50b239a 100644 --- a/server/middleware/src/configuration.ts +++ b/server/middleware/src/configuration.ts @@ -29,7 +29,7 @@ import core, { TxCUD } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' +import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' import { BaseMiddleware } from './base' const configurationAccountEmail = '#configurator@hc.engineering' @@ -45,6 +45,7 @@ export class ConfigurationMiddleware extends BaseMiddleware implements Middlewar static async create ( ctx: MeasureContext, + broadcast: BroadcastFunc, storage: ServerStorage, next?: Middleware ): Promise { diff --git a/server/middleware/src/modified.ts b/server/middleware/src/modified.ts index add0efcba5..29ffca03a2 100644 --- a/server/middleware/src/modified.ts +++ b/server/middleware/src/modified.ts @@ -23,7 +23,7 @@ import core, { TxCollectionCUD, TxCreateDoc } from '@hcengineering/core' -import { Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' +import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' import { BaseMiddleware } from './base' /** @@ -34,7 +34,12 @@ export class ModifiedMiddleware extends BaseMiddleware implements Middleware { super(storage, next) } - static async create (ctx: MeasureContext, storage: ServerStorage, next?: Middleware): Promise { + static async create ( + ctx: MeasureContext, + broadcast: BroadcastFunc, + storage: ServerStorage, + next?: Middleware + ): Promise { return new ModifiedMiddleware(storage, next) } diff --git a/server/middleware/src/private.ts b/server/middleware/src/private.ts index 8b5188207c..3838829dd7 100644 --- a/server/middleware/src/private.ts +++ b/server/middleware/src/private.ts @@ -29,7 +29,7 @@ import core, { TxCUD } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' +import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference' import { BaseMiddleware } from './base' import { getUser, mergeTargets } from './utils' @@ -44,7 +44,12 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware { super(storage, next) } - static async create (ctx: MeasureContext, storage: ServerStorage, next?: Middleware): Promise { + static async create ( + ctx: MeasureContext, + broadcast: BroadcastFunc, + storage: ServerStorage, + next?: Middleware + ): Promise { return new PrivateMiddleware(storage, next) } diff --git a/server/middleware/src/queryJoin.ts b/server/middleware/src/queryJoin.ts index 7ee17aeebb..9a51ea4624 100644 --- a/server/middleware/src/queryJoin.ts +++ b/server/middleware/src/queryJoin.ts @@ -24,7 +24,7 @@ import { ServerStorage, Tx } from '@hcengineering/core' -import { Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' +import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' import { BaseMiddleware } from './base' import { deepEqual } from 'fast-equals' @@ -47,7 +47,12 @@ export class QueryJoinMiddleware extends BaseMiddleware implements Middleware { super(storage, next) } - static async create (ctx: MeasureContext, storage: ServerStorage, next?: Middleware): Promise { + static async create ( + ctx: MeasureContext, + broadcast: BroadcastFunc, + storage: ServerStorage, + next?: Middleware + ): Promise { return new QueryJoinMiddleware(storage, next) } diff --git a/server/middleware/src/spaceSecurity.ts b/server/middleware/src/spaceSecurity.ts index 4e9a603bfb..acf297083d 100644 --- a/server/middleware/src/spaceSecurity.ts +++ b/server/middleware/src/spaceSecurity.ts @@ -21,6 +21,7 @@ import core, { DocumentQuery, FindOptions, FindResult, + generateId, LookupData, MeasureContext, ObjQueryType, @@ -34,10 +35,12 @@ import core, { TxCUD, TxProcessor, TxRemoveDoc, - TxUpdateDoc + TxUpdateDoc, + TxWorkspaceEvent, + WorkspaceEvent } from '@hcengineering/core' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' -import { Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' +import { BroadcastFunc, Middleware, SessionContext, TxMiddlewareResult } from '@hcengineering/server-core' import { BaseMiddleware } from './base' import { getUser, isOwner, mergeTargets } from './utils' @@ -56,16 +59,17 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar core.space.Tx ] - private constructor (storage: ServerStorage, next?: Middleware) { + private constructor (private readonly broadcast: BroadcastFunc, storage: ServerStorage, next?: Middleware) { super(storage, next) } static async create ( ctx: MeasureContext, + broadcast: BroadcastFunc, storage: ServerStorage, next?: Middleware ): Promise { - const res = new SpaceSecurityMiddleware(storage, next) + const res = new SpaceSecurityMiddleware(broadcast, storage, next) await res.init(ctx) return res } @@ -125,42 +129,58 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } - private pushMembersHandle (addedMembers: Ref | Position>, space: Ref): void { + private async pushMembersHandle ( + addedMembers: Ref | Position>, + space: Ref + ): Promise { if (typeof addedMembers === 'object') { for (const member of addedMembers.$each) { this.addMemberSpace(member, space) } + await this.brodcastEvent(addedMembers.$each) } else { this.addMemberSpace(addedMembers, space) + await this.brodcastEvent([addedMembers]) } } - private pullMembersHandle (removedMembers: Partial> | PullArray>, space: Ref): void { + private async pullMembersHandle ( + removedMembers: Partial> | PullArray>, + space: Ref + ): Promise { if (typeof removedMembers === 'object') { const { $in } = removedMembers as PullArray> if ($in !== undefined) { for (const member of $in) { this.removeMemberSpace(member, space) } + await this.brodcastEvent($in) } } else { this.removeMemberSpace(removedMembers, space) + await this.brodcastEvent([removedMembers]) } } - private syncMembers (members: Ref[], space: Space): void { + private async syncMembers (members: Ref[], space: Space): Promise { const oldMembers = new Set(space.members) const newMembers = new Set(members) + const changed: Ref[] = [] for (const old of oldMembers) { if (!newMembers.has(old)) { this.removeMemberSpace(old, space._id) + changed.push(old) } } for (const newMem of newMembers) { if (!oldMembers.has(newMem)) { this.addMemberSpace(newMem, space._id) + changed.push(newMem) } } + if (changed.length > 0) { + await this.brodcastEvent(changed) + } } private removePublicSpace (_id: Ref): void { @@ -170,6 +190,26 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } + private async brodcastEvent (users: Ref[]): Promise { + const targets = await this.getTargets(users) + const tx: TxWorkspaceEvent = { + _class: core.class.TxWorkspaceEvent, + _id: generateId(), + event: WorkspaceEvent.SecurityChange, + modifiedBy: core.account.System, + modifiedOn: Date.now(), + objectSpace: core.space.DerivedTx, + space: core.space.DerivedTx, + params: null + } + this.broadcast([tx], targets) + } + + private async broadcastNonMembers (space: Space | undefined): Promise { + const users = await this.storage.modelDb.findAll(core.class.Account, { _id: { $nin: space?.members } }) + await this.brodcastEvent(users.map((p) => p._id)) + } + private async handleUpdate (ctx: SessionContext, tx: TxCUD): Promise { const updateDoc = tx as TxUpdateDoc if (!this.storage.hierarchy.isDerived(updateDoc.objectClass, core.class.Space)) return @@ -181,24 +221,27 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar res.private = true this.addSpace(res) this.removePublicSpace(res._id) + await this.broadcastNonMembers(res) } } else if (!updateDoc.operations.private) { + const space = this.privateSpaces[updateDoc.objectId] this.removeSpace(updateDoc.objectId) this.publicSpaces.push(updateDoc.objectId) + await this.broadcastNonMembers(space) } } let space = this.privateSpaces[updateDoc.objectId] if (space !== undefined) { if (updateDoc.operations.members !== undefined) { - this.syncMembers(updateDoc.operations.members, space) + await this.syncMembers(updateDoc.operations.members, space) } if (updateDoc.operations.$push?.members !== undefined) { - this.pushMembersHandle(updateDoc.operations.$push.members, space._id) + await this.pushMembersHandle(updateDoc.operations.$push.members, space._id) } if (updateDoc.operations.$pull?.members !== undefined) { - this.pullMembersHandle(updateDoc.operations.$pull.members, space._id) + await this.pullMembersHandle(updateDoc.operations.$pull.members, space._id) } space = TxProcessor.updateDoc2Doc(space, updateDoc) } @@ -259,6 +302,12 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar } } } + if (h.isDerived(cudTx.objectClass, core.class.Account) && cudTx._class === core.class.TxUpdateDoc) { + const ctx = cudTx as TxUpdateDoc + if (ctx.operations.role !== undefined) { + await this.brodcastEvent([ctx.objectId]) + } + } } const res = await this.provideTx(ctx, tx) diff --git a/server/ws/src/server.ts b/server/ws/src/server.ts index 61061ace5f..fcbc785787 100644 --- a/server/ws/src/server.ts +++ b/server/ws/src/server.ts @@ -151,11 +151,13 @@ class TSessionManager implements SessionManager { console.log(token.workspace.name, 'no sessions for workspace', wsString) } // Re-create pipeline. - workspace.pipeline = pipelineFactory(ctx, token.workspace, true, (tx) => this.broadcastAll(workspace, tx)) + workspace.pipeline = pipelineFactory(ctx, token.workspace, true, (tx, targets) => + this.broadcastAll(workspace, tx, targets) + ) return await workspace.pipeline } - broadcastAll (workspace: Workspace, tx: Tx[]): void { + broadcastAll (workspace: Workspace, tx: Tx[], targets?: string[]): void { if (workspace?.upgrade ?? false) { return } @@ -163,6 +165,7 @@ class TSessionManager implements SessionManager { const sessions = [...workspace.sessions.values()] function send (): void { for (const session of sessions.splice(0, 1)) { + if (targets !== undefined && !targets.includes(session.session.getUser())) continue for (const _tx of tx) { void session.socket.send(ctx, { result: _tx }, session.session.binaryResponseMode, false) } @@ -180,7 +183,9 @@ class TSessionManager implements SessionManager { const upgrade = token.extra?.model === 'upgrade' const workspace: Workspace = { id: generateId(), - pipeline: pipelineFactory(ctx, token.workspace, upgrade, (tx) => this.broadcastAll(workspace, tx)), + pipeline: pipelineFactory(ctx, token.workspace, upgrade, (tx, targets) => + this.broadcastAll(workspace, tx, targets) + ), sessions: new Map(), upgrade } diff --git a/server/ws/src/types.ts b/server/ws/src/types.ts index 0619303ee0..df1be22121 100644 --- a/server/ws/src/types.ts +++ b/server/ws/src/types.ts @@ -11,7 +11,7 @@ import { WorkspaceId } from '@hcengineering/core' import { Response } from '@hcengineering/rpc' -import { Pipeline } from '@hcengineering/server-core' +import { BroadcastFunc, Pipeline } from '@hcengineering/server-core' import { Token } from '@hcengineering/server-token' /** @@ -67,7 +67,7 @@ export type PipelineFactory = ( ctx: MeasureContext, ws: WorkspaceId, upgrade: boolean, - broadcast: (tx: Tx[]) => void + broadcast: BroadcastFunc ) => Promise /** @@ -120,7 +120,7 @@ export interface SessionManager { sessionId?: string ) => Promise - broadcastAll: (workspace: Workspace, tx: Tx[]) => void + broadcastAll: (workspace: Workspace, tx: Tx[], targets?: string[]) => void close: ( ctx: MeasureContext,