mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-26 03:25:01 +02:00
+1








98652c6476
* Add bump-changes * Add utility tests * Add utility tests * Bump to new version of esbuild and typescript * v0.7.3 * use platform rig 0.7.10 * upgrade: memory engine optimized; change name to (was recommended by Copilot and Onnikov, TODO: CHANGE CLIENT TOO!!!) Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix rate limits bug * Bump versions * Fix lock file * Fix bug in queue cleanup * Add more tests for queue * Add api-test tests * Initial commit * Improve hierarchy + tests Add tests for hierarchy and few performance/memory optimizations. * Add more hierarchy tests * Move from Huly platform repository * Add docker tests setup * Fix test to be executed only once * Add connection tests * Fix package include source files * More tests * Create README.md * Fix pnpm lock * Fix packages publish * Remove broken tests * feat: adjust hulylake client for storage adapter Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix export * Fix publish * Fix message update (#114) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix lang store (#115) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * update hulylake client Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump version Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add hulylake storage adapter Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump version Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix validation issues Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix: do not fail on deseralization error and add logs Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.14 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix collaboration test Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Update prettier and new update-deps script Prettier + svelte support * fix unstable ydoc tests Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add tx ordering middleware * Fix ordering tests * Fix Kafka close of admin * Add tests for measurement and understand overhead * Fix not updated lock file * Fix update-deps * Fix update-deps * Use latest platform-rig * Fix deps * Add rush check to CI * Use latest versions * Bump versions * Fix lock file * validate json patch Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.15 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix merge unit tests Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Script to sync eslint deps * Fix deps * Fix tests * Fix platform-rig detection * Update to latest platform-rig * Update to latest platform rig and core * Bump typescript * Bump typescript * Rollback eslint plugins * Fix lock file * Bump platform-rig * Update to latest platform-rig * update to latest platform-rig * Allow to compile svelte files * Add ui-test component for checking compile * Fix log levels rename compile ui -> compile ui-esbuild * Fix build * Bump esbuild svelte version * Chore: use fixed versions in update-deps Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Chore: commit changes Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Update deps * Add tests for session manager * Fix txOrdering implementation * Bump ordering * Prevent metrics zero values in measure + Fix format svelte files * Revert update-deps script logic * v0.7.19 * update to latest platform-rig * Update deps * Fix pnpm * Session counters * Fix pnpm lock * Add storage client Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump core * Fix pnpm * Get rid of communication dependency * Add copilot memory file * Use proper name for instructions file * Fix instructions * Use domain instead of test name in gauges * Update instructions file * fix front service upload Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * remove incorrect test Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Move packages to huly.core * Move packages to core, since they are not utils * Add global user profile Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Add support for memory limit check * Bump version * Fix pnpm * report more accurate upload progress Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fic validation issues Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix deps Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Move LowLevelStorage to server * Fix linting * Revert "Fix linting" This reverts commit54631d353e. * Revert "Move LowLevelStorage to server" This reverts commitaafb8f6f12. * feature: add regorus engine with permit file Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix one second counters for memory usage * Fix kafka test * use fresh core * Version bump * fix: key parameter added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix readme and few author mistakes * Export domain schemas * Bump version * Tests (#117) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * feat: compact compact worker (#4) Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.16 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add TypeIdentifier Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add change logs Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * rename send -> try_send Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix pnpm lock Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add identifier middleware, bump core Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add subsciption methods to account client Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix reaction notification (#118) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Improve find methods schemas to convert to valid types Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> * Add change description Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> * Do not transcode while recording Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Open telemetry support Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * use proper content type in multipart upload Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix build (#26) Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix peers (#120) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Add ActivityCollaborativeChange Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update pnpm Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Allow to suspend errors on with * Fix pnpm cache * update versions * v0.7.17 for all * v0.7.11 * v0.7.14 * remove arc from worker Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.17 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Rank for attributes Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update pnpm Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix one second counters * Fix withContext and allow pass options * Fix formatting * Use updated deps * Bump versions * Update deps * Update deps to platform.core * add support for textColor mark Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * add support for textStyle mark Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions again Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Rework on second timers * fix merge of large blobs feched from s3 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.18 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * New subscription methods in account-client * Update lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Send error on find for wrong domain * Suspend connect custom errors events in traces * Bump client * Bump core * update deps * Fix lock file * Sorting for TypeIdentifier Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Bump version Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * add workspace usage info Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Improve pg security perfomance Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix identifier middleware Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update TxAccessLevel interface Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Allow guest to update its identities Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Add password login locked platform status Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix Uptrace normalizeMarkdown errors Signed-off-by: Artem Savchenko <armisav@gmail.com> * Add change log Signed-off-by: Artem Savchenko <armisav@gmail.com> * Add txMatch to permission Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * update pnpm lock Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix permission middleware Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix enum sorting Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Enable formatting check Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Enable formatting check * Add change Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Fix Uptrace NaN error Signed-off-by: Artem Savchenko <armisav@gmail.com> * feature: removed actors, improved performance Signed-off-by: Leonid Kaganov <lleo@lleo.me> * feature: ping from server to clients added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * feature: ping from server to clients added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Compress kafka messages and fix exception in findAll Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump versions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump versions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Rush change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix compression param Signed-off-by: Artem Savchenko <armisav@gmail.com> * Trigger change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Clean up Signed-off-by: Artem Savchenko <armisav@gmail.com> * Trigger change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump markdown version Signed-off-by: Artem Savchenko <armisav@gmail.com> * Enable sub projects * Fix wrong double symbol scripts * Include foundation packages * Add support for custom exclude filters Add support for custom exclude filters - by Andrey Sobolev - haiodo@gmail.com Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix Uptrace filter is not a function error Signed-off-by: Artem Savchenko <armisav@gmail.com> * Sync versions Signed-off-by: Andrey Sobolev <haiodo@gmail.com> --------- Signed-off-by: Leonid Kaganov <lleo@lleo.me> Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> Signed-off-by: Artem Savchenko <armisav@gmail.com> Signed-off-by: Andrey Sobolev <haiodo@gmail.com> Co-authored-by: Leonid Kaganov <lleo@lleo.me> Co-authored-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Co-authored-by: Alexander Onnikov <Alexander.Onnikov@gmail.com> Co-authored-by: Kristina <kristin.fefelova@gmail.com> Co-authored-by: Alexey Zinoviev <alexey.zinoviev@xored.com> Co-authored-by: Denis Bykhov <bykhov.denis@gmail.com> Co-authored-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> Co-authored-by: Alexander Onnikov <aonnikov@hardcoreeng.com> Co-authored-by: Artem Savchenko <armisav@gmail.com>
743 lines
25 KiB
TypeScript
743 lines
25 KiB
TypeScript
//
|
|
// Copyright © 2023 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
import core, {
|
|
type Account,
|
|
AccountRole,
|
|
type AccountUuid,
|
|
type AttachedDoc,
|
|
type Class,
|
|
clone,
|
|
type Collaborator,
|
|
type Doc,
|
|
type DocumentQuery,
|
|
type Domain,
|
|
DOMAIN_MODEL,
|
|
type FindResult,
|
|
generateId,
|
|
getClassCollaborators,
|
|
type LookupData,
|
|
type MeasureContext,
|
|
type ObjQueryType,
|
|
type Position,
|
|
type PullArray,
|
|
type Ref,
|
|
type SearchOptions,
|
|
type SearchQuery,
|
|
type SearchResult,
|
|
type SessionData,
|
|
shouldShowArchived,
|
|
type Space,
|
|
systemAccountUuid,
|
|
toFindResult,
|
|
type Tx,
|
|
type TxCreateDoc,
|
|
type TxCUD,
|
|
TxProcessor,
|
|
type TxRemoveDoc,
|
|
type TxUpdateDoc,
|
|
type TxWorkspaceEvent,
|
|
WorkspaceEvent
|
|
} from '@hcengineering/core'
|
|
import {
|
|
BaseMiddleware,
|
|
type Middleware,
|
|
type PipelineContext,
|
|
type ServerFindOptions,
|
|
type TxMiddlewareResult
|
|
} from '@hcengineering/server-core'
|
|
import { isOwner, isSystem } from './utils'
|
|
|
|
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class' | 'archived'>
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export class SpaceSecurityMiddleware extends BaseMiddleware implements Middleware {
|
|
private allowedSpaces: Record<AccountUuid, Ref<Space>[]> = {}
|
|
private readonly spacesMap = new Map<Ref<Space>, SpaceWithMembers>()
|
|
private readonly privateSpaces = new Set<Ref<Space>>()
|
|
private readonly _domainSpaces = new Map<string, Set<Ref<Space>> | Promise<Set<Ref<Space>>>>()
|
|
private readonly publicSpaces = new Set<Ref<Space>>()
|
|
private readonly systemSpaces = new Set<Ref<Space>>()
|
|
|
|
wasInit: Promise<void> | boolean = false
|
|
|
|
private readonly mainSpaces = new Set([
|
|
core.space.Configuration,
|
|
core.space.DerivedTx,
|
|
core.space.Model,
|
|
core.space.Space,
|
|
core.space.Workspace,
|
|
core.space.Tx
|
|
])
|
|
|
|
private constructor (
|
|
private readonly skipFindCheck: boolean,
|
|
context: PipelineContext,
|
|
next?: Middleware
|
|
) {
|
|
super(context, next)
|
|
}
|
|
|
|
static async create (
|
|
skipFindCheck: boolean,
|
|
ctx: MeasureContext,
|
|
context: PipelineContext,
|
|
next: Middleware | undefined
|
|
): Promise<SpaceSecurityMiddleware> {
|
|
return new SpaceSecurityMiddleware(skipFindCheck, context, next)
|
|
}
|
|
|
|
private resyncDomains (): void {
|
|
this.wasInit = false
|
|
}
|
|
|
|
private addMemberSpace (member: AccountUuid, space: Ref<Space>): void {
|
|
const arr = this.allowedSpaces[member] ?? []
|
|
arr.push(space)
|
|
this.allowedSpaces[member] = arr
|
|
}
|
|
|
|
private addSpace (space: SpaceWithMembers): void {
|
|
this.spacesMap.set(space._id, space)
|
|
if (space.private) {
|
|
this.privateSpaces.add(space._id)
|
|
} else {
|
|
this.publicSpaces.add(space._id)
|
|
}
|
|
for (const member of space.members) {
|
|
this.addMemberSpace(member, space._id)
|
|
}
|
|
}
|
|
|
|
async init (ctx: MeasureContext): Promise<void> {
|
|
if (this.wasInit === true) {
|
|
return
|
|
}
|
|
if (this.wasInit === false) {
|
|
this.wasInit = (async () => {
|
|
await ctx.with('init-space-security', {}, async (ctx) => {
|
|
ctx.contextData = undefined
|
|
const spaces: SpaceWithMembers[] =
|
|
(await this.next?.findAll(
|
|
ctx,
|
|
core.class.Space,
|
|
{},
|
|
{
|
|
projection: {
|
|
archived: 1,
|
|
private: 1,
|
|
_class: 1,
|
|
_id: 1,
|
|
members: 1
|
|
}
|
|
}
|
|
)) ?? []
|
|
this.spacesMap.clear()
|
|
this.publicSpaces.clear()
|
|
this.systemSpaces.clear()
|
|
for (const space of spaces) {
|
|
if (space._class === core.class.SystemSpace) {
|
|
this.systemSpaces.add(space._id)
|
|
} else {
|
|
this.addSpace(space)
|
|
}
|
|
}
|
|
})
|
|
})()
|
|
}
|
|
if (this.wasInit instanceof Promise) {
|
|
await this.wasInit
|
|
this.wasInit = true
|
|
}
|
|
}
|
|
|
|
private removeMemberSpace (member: AccountUuid, space: Ref<Space>): void {
|
|
const arr = this.allowedSpaces[member]
|
|
if (arr !== undefined) {
|
|
const index = arr.findIndex((p) => p === space)
|
|
if (index !== -1) {
|
|
arr.splice(index, 1)
|
|
this.allowedSpaces[member] = arr
|
|
}
|
|
}
|
|
}
|
|
|
|
private removeSpace (_id: Ref<Space>): void {
|
|
const space = this.spacesMap.get(_id)
|
|
if (space !== undefined) {
|
|
for (const member of space.members) {
|
|
this.removeMemberSpace(member, space._id)
|
|
}
|
|
}
|
|
this.spacesMap.delete(_id)
|
|
this.privateSpaces.delete(_id)
|
|
this.publicSpaces.delete(_id)
|
|
}
|
|
|
|
private async handeCollaborator (ctx: MeasureContext<SessionData>, tx: TxCUD<Collaborator>): Promise<void> {
|
|
if (!this.context.hierarchy.isDerived(tx.objectClass, core.class.Collaborator)) return
|
|
if (tx._class === core.class.TxCreateDoc) {
|
|
const collab = TxProcessor.createDoc2Doc<Collaborator>(tx as TxCreateDoc<Collaborator>)
|
|
this.handleChangeCollaborator(ctx, collab)
|
|
} else if (tx._class === core.class.TxRemoveDoc) {
|
|
const collab = (await this.next?.findAll(ctx, core.class.Collaborator, {
|
|
_id: tx.objectId
|
|
})) as Collaborator[]
|
|
if (collab.length === 0) return
|
|
this.handleChangeCollaborator(ctx, collab[0])
|
|
}
|
|
}
|
|
|
|
private handleChangeCollaborator (ctx: MeasureContext<SessionData>, collab: Collaborator): void {
|
|
const collabSec = this.context.modelDb.findAllSync(core.class.ClassCollaborators, {
|
|
attachedTo: collab.attachedToClass
|
|
})[0]
|
|
if (collabSec?.provideSecurity === true) {
|
|
for (const val of ctx.contextData.socialStringsToUsers.values()) {
|
|
if (
|
|
val.accontUuid === collab.collaborator &&
|
|
[AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)
|
|
) {
|
|
this.brodcastEvent(ctx, [val.accontUuid])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private handleCreate (tx: TxCUD<Space>): void {
|
|
const createTx = tx as TxCreateDoc<Space>
|
|
if (!this.context.hierarchy.isDerived(createTx.objectClass, core.class.Space)) return
|
|
if (createTx.objectClass === core.class.SystemSpace) {
|
|
this.systemSpaces.add(createTx.objectId)
|
|
} else {
|
|
const res = TxProcessor.createDoc2Doc<Space>(createTx)
|
|
this.addSpace(res)
|
|
}
|
|
}
|
|
|
|
private pushMembersHandle (
|
|
ctx: MeasureContext,
|
|
addedMembers: AccountUuid | Position<AccountUuid>,
|
|
space: Ref<Space>
|
|
): void {
|
|
if (typeof addedMembers === 'object') {
|
|
for (const member of addedMembers.$each) {
|
|
this.addMemberSpace(member, space)
|
|
}
|
|
this.brodcastEvent(ctx, addedMembers.$each, space)
|
|
} else {
|
|
this.addMemberSpace(addedMembers, space)
|
|
this.brodcastEvent(ctx, [addedMembers], space)
|
|
}
|
|
}
|
|
|
|
private pullMembersHandle (
|
|
ctx: MeasureContext,
|
|
removedMembers: Partial<AccountUuid> | PullArray<AccountUuid>,
|
|
space: Ref<Space>
|
|
): void {
|
|
if (typeof removedMembers === 'object') {
|
|
const { $in } = removedMembers as PullArray<AccountUuid>
|
|
if ($in !== undefined) {
|
|
for (const member of $in) {
|
|
this.removeMemberSpace(member, space)
|
|
}
|
|
this.brodcastEvent(ctx, $in, space)
|
|
}
|
|
} else {
|
|
this.removeMemberSpace(removedMembers, space)
|
|
this.brodcastEvent(ctx, [removedMembers], space)
|
|
}
|
|
}
|
|
|
|
private syncMembers (ctx: MeasureContext, members: AccountUuid[], space: SpaceWithMembers): void {
|
|
const oldMembers = new Set(space.members)
|
|
const newMembers = new Set(members)
|
|
const changed: AccountUuid[] = []
|
|
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)
|
|
}
|
|
}
|
|
// TODO: consider checking if updated social strings actually change assigned accounts
|
|
if (changed.length > 0) {
|
|
this.brodcastEvent(ctx, changed, space._id)
|
|
}
|
|
}
|
|
|
|
private brodcastEvent (ctx: MeasureContext<SessionData>, users: AccountUuid[], space?: Ref<Space>): void {
|
|
const targets = this.getTargets(users)
|
|
const tx: TxWorkspaceEvent = {
|
|
_class: core.class.TxWorkspaceEvent,
|
|
_id: generateId(),
|
|
event: WorkspaceEvent.SecurityChange,
|
|
modifiedBy: core.account.System,
|
|
modifiedOn: Date.now(),
|
|
objectSpace: space ?? core.space.DerivedTx,
|
|
space: core.space.DerivedTx,
|
|
params: null
|
|
}
|
|
ctx.contextData.broadcast.txes.push(tx)
|
|
ctx.contextData.broadcast.targets['security' + tx._id] = async (it) => {
|
|
// TODO: I'm not sure it is called
|
|
if (it._id === tx._id) {
|
|
return {
|
|
target: targets
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private broadcastNonMembers (ctx: MeasureContext<SessionData>, space: SpaceWithMembers): void {
|
|
const members = space?.members ?? []
|
|
|
|
this.brodcastEvent(ctx, members, space._id)
|
|
}
|
|
|
|
private broadcastAll (ctx: MeasureContext<SessionData>, space: SpaceWithMembers): void {
|
|
const { socialStringsToUsers } = ctx.contextData
|
|
const accounts = Array.from(new Set(Array.from(socialStringsToUsers.values()).map((v) => v.accontUuid)))
|
|
|
|
this.brodcastEvent(ctx, accounts, space._id)
|
|
}
|
|
|
|
private async handleUpdate (ctx: MeasureContext, tx: TxCUD<Space>): Promise<void> {
|
|
await this.init(ctx)
|
|
|
|
const updateDoc = tx as TxUpdateDoc<Space>
|
|
if (!this.context.hierarchy.isDerived(updateDoc.objectClass, core.class.Space)) return
|
|
|
|
const space = this.spacesMap.get(updateDoc.objectId)
|
|
if (space !== undefined) {
|
|
if (updateDoc.operations.private !== undefined) {
|
|
if (updateDoc.operations.private) {
|
|
this.privateSpaces.add(updateDoc.objectId)
|
|
this.publicSpaces.delete(updateDoc.objectId)
|
|
this.broadcastNonMembers(ctx, space)
|
|
} else if (!updateDoc.operations.private) {
|
|
this.privateSpaces.delete(updateDoc.objectId)
|
|
this.publicSpaces.add(updateDoc.objectId)
|
|
this.broadcastNonMembers(ctx, space)
|
|
}
|
|
}
|
|
|
|
if (updateDoc.operations.members !== undefined) {
|
|
this.syncMembers(ctx, updateDoc.operations.members, space)
|
|
}
|
|
if (updateDoc.operations.$push?.members !== undefined) {
|
|
this.pushMembersHandle(ctx, updateDoc.operations.$push.members, space._id)
|
|
}
|
|
|
|
if (updateDoc.operations.$pull?.members !== undefined) {
|
|
this.pullMembersHandle(ctx, updateDoc.operations.$pull.members, space._id)
|
|
}
|
|
if (updateDoc.operations.archived !== undefined) {
|
|
this.broadcastAll(ctx, space)
|
|
}
|
|
const updatedSpace = TxProcessor.updateDoc2Doc(space as any, updateDoc)
|
|
this.spacesMap.set(updateDoc.objectId, updatedSpace)
|
|
}
|
|
}
|
|
|
|
private handleRemove (tx: TxCUD<Space>): void {
|
|
const removeTx = tx as TxRemoveDoc<Space>
|
|
if (!this.context.hierarchy.isDerived(removeTx.objectClass, core.class.Space)) return
|
|
if (removeTx._class !== core.class.TxRemoveDoc) return
|
|
this.removeSpace(tx.objectId)
|
|
}
|
|
|
|
private async handleTx (ctx: MeasureContext, tx: TxCUD<Space>): Promise<void> {
|
|
await this.init(ctx)
|
|
if (tx._class === core.class.TxCreateDoc) {
|
|
this.handleCreate(tx)
|
|
} else if (tx._class === core.class.TxUpdateDoc) {
|
|
await this.handleUpdate(ctx, tx)
|
|
} else if (tx._class === core.class.TxRemoveDoc) {
|
|
this.handleRemove(tx)
|
|
}
|
|
}
|
|
|
|
getTargets (accounts: AccountUuid[]): AccountUuid[] {
|
|
const res = Array.from(new Set(accounts))
|
|
// We need to add system account for targets for integrations to work properly
|
|
res.push(systemAccountUuid)
|
|
|
|
return res
|
|
}
|
|
|
|
private async processTxSpaceDomain (sctx: MeasureContext, actualTx: TxCUD<Doc>): Promise<void> {
|
|
if (actualTx._class === core.class.TxCreateDoc) {
|
|
const ctx = actualTx as TxCreateDoc<Doc>
|
|
const doc = TxProcessor.createDoc2Doc(ctx)
|
|
const domain = this.context.hierarchy.getDomain(ctx.objectClass)
|
|
const key = this.getKey(domain)
|
|
const space = (doc as any)[key]
|
|
if (space === undefined) return
|
|
;(await this.getDomainSpaces(sctx, domain)).add(space)
|
|
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
|
const updTx = actualTx as TxUpdateDoc<Doc>
|
|
const domain = this.context.hierarchy.getDomain(updTx.objectClass)
|
|
const key = this.getKey(domain)
|
|
const space = (updTx.operations as any)[key]
|
|
if (space !== undefined) {
|
|
;(await this.getDomainSpaces(sctx, domain)).add(space)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async processTx (ctx: MeasureContext<SessionData>, tx: Tx): Promise<void> {
|
|
const h = this.context.hierarchy
|
|
if (TxProcessor.isExtendsCUD(tx._class)) {
|
|
const cudTx = tx as TxCUD<Doc>
|
|
const isSpace = h.isDerived(cudTx.objectClass, core.class.Space)
|
|
if (isSpace) {
|
|
await this.handleTx(ctx, cudTx as TxCUD<Space>)
|
|
} else {
|
|
await this.handeCollaborator(ctx, cudTx as TxCUD<Collaborator>)
|
|
}
|
|
await this.processTxSpaceDomain(ctx, tx as TxCUD<Doc>)
|
|
} else if (tx._class === core.class.TxWorkspaceEvent) {
|
|
const event = tx as TxWorkspaceEvent
|
|
if (event.event === WorkspaceEvent.BulkUpdate) {
|
|
this.resyncDomains()
|
|
}
|
|
}
|
|
}
|
|
|
|
async tx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxMiddlewareResult> {
|
|
await this.init(ctx)
|
|
const processed = new Set<Ref<Tx>>()
|
|
ctx.contextData.contextCache.set('processed', processed)
|
|
for (const tx of txes) {
|
|
processed.add(tx._id)
|
|
await this.processTx(ctx, tx)
|
|
}
|
|
return await this.provideTx(ctx, txes)
|
|
}
|
|
|
|
override async handleBroadcast (ctx: MeasureContext<SessionData>): Promise<void> {
|
|
const processed: Set<Ref<Tx>> = ctx.contextData.contextCache.get('processed') ?? new Set<Ref<Tx>>()
|
|
ctx.contextData.contextCache.set('processed', processed)
|
|
for (const txd of ctx.contextData.broadcast.txes) {
|
|
if (!processed.has(txd._id)) {
|
|
await this.processTx(ctx, txd)
|
|
}
|
|
}
|
|
for (const tx of ctx.contextData.broadcast.txes) {
|
|
if (TxProcessor.isExtendsCUD(tx._class)) {
|
|
// TODO: Do we need security check here?
|
|
const cudTx = tx as TxCUD<Doc>
|
|
await this.processTxSpaceDomain(ctx, cudTx)
|
|
} else if (tx._class === core.class.TxWorkspaceEvent) {
|
|
const event = tx as TxWorkspaceEvent
|
|
if (event.event === WorkspaceEvent.BulkUpdate) {
|
|
this.resyncDomains()
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.contextData.broadcast.targets.spaceSec = async (tx) => {
|
|
const cud = tx as TxCUD<Doc>
|
|
if (cud.objectClass === undefined) return undefined
|
|
|
|
// For system and main spaces broadcast to all users except guests that are not collaborators for objects with collab security enabled
|
|
if (this.systemSpaces.has(tx.objectSpace) || this.mainSpaces.has(tx.objectSpace)) {
|
|
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
|
|
if (collabSec?.provideSecurity === true) {
|
|
const guests = new Set<AccountUuid>()
|
|
for (const val of ctx.contextData.socialStringsToUsers.values()) {
|
|
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) {
|
|
guests.add(val.accontUuid)
|
|
}
|
|
}
|
|
const collabs = (await this.next?.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: cud.objectId
|
|
})) as Collaborator[]
|
|
for (const collab of collabs) {
|
|
guests.delete(collab.collaborator)
|
|
}
|
|
return { exclude: Array.from(guests) }
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const space = this.spacesMap.get(tx.objectSpace)
|
|
if (space === undefined) return undefined
|
|
|
|
// For all other spaces broadcast to space members + guests that are collaborators for objects with collab security enabled
|
|
let collabTargets: AccountUuid[] = []
|
|
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
|
|
if (collabSec?.provideSecurity === true) {
|
|
const guests = new Set<AccountUuid>()
|
|
for (const val of ctx.contextData.socialStringsToUsers.values()) {
|
|
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) {
|
|
guests.add(val.accontUuid)
|
|
}
|
|
}
|
|
const collaboratorObjs = (await this.next?.findAll(ctx, core.class.Collaborator, {
|
|
attachedTo: cud.objectId
|
|
})) as Collaborator[]
|
|
|
|
collabTargets = collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it))
|
|
}
|
|
const spaceTargets = space.members.length === 0 ? [] : this.getTargets(space?.members)
|
|
const target = [...collabTargets, ...spaceTargets]
|
|
|
|
return target.length === 0 ? undefined : { target }
|
|
}
|
|
|
|
await this.next?.handleBroadcast(ctx)
|
|
}
|
|
|
|
private getAllAllowedSpaces (
|
|
account: Account,
|
|
isData: boolean,
|
|
showArchived: boolean,
|
|
forSearch: boolean = false
|
|
): Ref<Space>[] {
|
|
const userSpaces = this.allowedSpaces[account.uuid] ?? []
|
|
let res = [...Array.from(userSpaces), account.uuid as unknown as Ref<Space>, ...this.mainSpaces]
|
|
if (!forSearch || ![AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(account.role)) {
|
|
res = [...res, ...this.systemSpaces]
|
|
}
|
|
const ignorePublicSpaces = isData || account.role === AccountRole.ReadOnlyGuest
|
|
const unfilteredRes = ignorePublicSpaces ? res : [...res, ...this.publicSpaces]
|
|
if (showArchived) {
|
|
return unfilteredRes
|
|
}
|
|
return unfilteredRes.filter((p) => this.spacesMap.get(p)?.archived !== true)
|
|
}
|
|
|
|
async getDomainSpaces (ctx: MeasureContext, domain: Domain): Promise<Set<Ref<Space>>> {
|
|
let domainSpaces = this._domainSpaces.get(domain)
|
|
if (domainSpaces === undefined) {
|
|
const p = (
|
|
this.next?.groupBy<Ref<Space>, Doc>(ctx, domain, this.getKey(domain)) ?? Promise.resolve(new Map())
|
|
).then((r) => new Set<Ref<Space>>(r.keys()))
|
|
this._domainSpaces.set(domain, p)
|
|
domainSpaces = await p
|
|
this._domainSpaces.set(domain, domainSpaces)
|
|
}
|
|
return domainSpaces instanceof Promise ? await domainSpaces : domainSpaces
|
|
}
|
|
|
|
private async filterByDomain (
|
|
ctx: MeasureContext,
|
|
domain: Domain,
|
|
spaces: Ref<Space>[]
|
|
): Promise<{ result: Set<Ref<Space>>, allDomainSpaces: boolean, domainSpaces: Set<Ref<Space>> }> {
|
|
const domainSpaces = await this.getDomainSpaces(ctx, domain)
|
|
const result = new Set(spaces.filter((p) => domainSpaces.has(p)))
|
|
return {
|
|
result,
|
|
allDomainSpaces: result.size === domainSpaces.size,
|
|
domainSpaces
|
|
}
|
|
}
|
|
|
|
private async mergeQuery<T extends Doc>(
|
|
ctx: MeasureContext,
|
|
account: Account,
|
|
query: ObjQueryType<T['space']>,
|
|
domain: Domain,
|
|
isSpace: boolean,
|
|
showArchived: boolean
|
|
): Promise<ObjQueryType<T['space']> | undefined> {
|
|
const spaces = await this.filterByDomain(ctx, domain, this.getAllAllowedSpaces(account, !isSpace, showArchived))
|
|
if (query == null) {
|
|
if (spaces.allDomainSpaces) {
|
|
return undefined
|
|
}
|
|
return { $in: Array.from(spaces.result) }
|
|
}
|
|
if (typeof query === 'string') {
|
|
if (!spaces.result.has(query)) {
|
|
return { $in: [] }
|
|
}
|
|
} else if (query.$in != null) {
|
|
query.$in = query.$in.filter((p) => spaces.result.has(p))
|
|
if (query.$in.length === spaces.domainSpaces.size) {
|
|
// all domain spaces
|
|
delete query.$in
|
|
}
|
|
} else {
|
|
if (spaces.allDomainSpaces) {
|
|
delete query.$in
|
|
} else {
|
|
query.$in = Array.from(spaces.result)
|
|
}
|
|
}
|
|
if (Object.keys(query).length === 0) {
|
|
return undefined
|
|
}
|
|
return query
|
|
}
|
|
|
|
private getKey (domain: string): string {
|
|
return domain === 'tx' ? 'objectSpace' : domain === 'space' ? '_id' : 'space'
|
|
}
|
|
|
|
override async findAll<T extends Doc>(
|
|
ctx: MeasureContext<SessionData>,
|
|
_class: Ref<Class<T>>,
|
|
query: DocumentQuery<T>,
|
|
options?: ServerFindOptions<T>
|
|
): Promise<FindResult<T>> {
|
|
await this.init(ctx)
|
|
|
|
const domain = this.context.hierarchy.getDomain(_class)
|
|
const newQuery = clone(query)
|
|
const account = ctx.contextData.account
|
|
const isSpace = this.context.hierarchy.isDerived(_class, core.class.Space)
|
|
const field = this.getKey(domain)
|
|
const showArchived: boolean = shouldShowArchived(newQuery, options)
|
|
|
|
let clientFilterSpaces: Set<Ref<Space>> | undefined
|
|
|
|
if (!isSystem(account, ctx) && account.role !== AccountRole.DocGuest && domain !== DOMAIN_MODEL) {
|
|
if (!isOwner(account, ctx) || !isSpace || !showArchived) {
|
|
if (newQuery[field] !== undefined) {
|
|
const res = await this.mergeQuery(ctx, account, newQuery[field], domain, isSpace, showArchived)
|
|
if (res === undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete newQuery[field]
|
|
} else {
|
|
newQuery[field] = res
|
|
if (typeof res === 'object') {
|
|
if (Array.isArray(res.$in) && res.$in.length === 1 && Object.keys(res).length === 1) {
|
|
newQuery[field] = res.$in[0]
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const spaces = await this.filterByDomain(
|
|
ctx,
|
|
domain,
|
|
this.getAllAllowedSpaces(account, !isSpace, showArchived)
|
|
)
|
|
if (spaces.allDomainSpaces) {
|
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
delete newQuery[field]
|
|
} else if (spaces.result.size === 1) {
|
|
newQuery[field] = Array.from(spaces.result)[0]
|
|
if (options !== undefined) {
|
|
options.allowedSpaces = Array.from(spaces.result)
|
|
} else {
|
|
options = { allowedSpaces: Array.from(spaces.result) }
|
|
}
|
|
} else {
|
|
// Check if spaces are greater than 85% of all domain spaces. In this case, return all and filter on the client.
|
|
if (spaces.result.size / spaces.domainSpaces.size > 0.85 && options?.limit === undefined) {
|
|
clientFilterSpaces = spaces.result
|
|
delete newQuery.space
|
|
} else {
|
|
newQuery[field] = { $in: Array.from(spaces.result) }
|
|
if (options !== undefined) {
|
|
options.allowedSpaces = Array.from(spaces.result)
|
|
} else {
|
|
options = { allowedSpaces: Array.from(spaces.result) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let findResult = await this.provideFindAll(ctx, _class, !this.skipFindCheck ? newQuery : query, options)
|
|
if (clientFilterSpaces !== undefined) {
|
|
const cfs = clientFilterSpaces
|
|
findResult = toFindResult(
|
|
findResult.filter((it) => cfs.has((it as any)[field])),
|
|
findResult.total,
|
|
findResult.lookupMap
|
|
)
|
|
}
|
|
if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) {
|
|
if (options?.lookup !== undefined) {
|
|
for (const object of findResult) {
|
|
if (object.$lookup !== undefined) {
|
|
this.filterLookup(ctx, object.$lookup, showArchived)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return findResult
|
|
}
|
|
|
|
override async searchFulltext (
|
|
ctx: MeasureContext<SessionData>,
|
|
query: SearchQuery,
|
|
options: SearchOptions
|
|
): Promise<SearchResult> {
|
|
await this.init(ctx)
|
|
const newQuery = { ...query }
|
|
const account = ctx.contextData.account
|
|
if (!isSystem(account, ctx)) {
|
|
const allSpaces = this.getAllAllowedSpaces(account, true, false, true)
|
|
if (query.classes !== undefined) {
|
|
const res = new Set<Ref<Space>>()
|
|
const passedDomains = new Set<string>()
|
|
for (const _class of query.classes) {
|
|
const domain = this.context.hierarchy.getDomain(_class)
|
|
if (passedDomains.has(domain)) {
|
|
continue
|
|
}
|
|
passedDomains.add(domain)
|
|
const spaces = await this.filterByDomain(ctx, domain, allSpaces)
|
|
for (const space of spaces.result) {
|
|
res.add(space)
|
|
}
|
|
}
|
|
newQuery.spaces = [...res]
|
|
} else {
|
|
newQuery.spaces = allSpaces
|
|
}
|
|
}
|
|
const result = await this.provideSearchFulltext(ctx, newQuery, options)
|
|
return result
|
|
}
|
|
|
|
filterLookup<T extends Doc>(ctx: MeasureContext, lookup: LookupData<T>, showArchived: boolean): void {
|
|
if (Object.keys(lookup).length === 0) return
|
|
const account = ctx.contextData.account
|
|
if (isSystem(account, ctx)) return
|
|
const allowedSpaces = new Set(this.getAllAllowedSpaces(account, true, showArchived))
|
|
for (const key in lookup) {
|
|
const val = lookup[key]
|
|
if (Array.isArray(val)) {
|
|
const arr: AttachedDoc[] = []
|
|
for (const value of val) {
|
|
if (allowedSpaces.has(value.space)) {
|
|
arr.push(value)
|
|
}
|
|
}
|
|
lookup[key] = arr as any
|
|
} else if (val !== undefined) {
|
|
if (!allowedSpaces.has(val.space)) {
|
|
lookup[key] = undefined
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|