From b3b965a4d14d6cd1472fb0bf9558b0cd785b0f06 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Sat, 29 Nov 2025 15:34:33 +0500 Subject: [PATCH] Strict RBAC for spaces Signed-off-by: Denis Bykhov --- .vscode/launch.json | 2 +- foundations/core/packages/core/lang/cs.json | 2 + foundations/core/packages/core/lang/de.json | 2 + foundations/core/packages/core/lang/en.json | 2 + foundations/core/packages/core/lang/es.json | 2 + foundations/core/packages/core/lang/fr.json | 2 + foundations/core/packages/core/lang/it.json | 2 + foundations/core/packages/core/lang/ja.json | 2 + foundations/core/packages/core/lang/pt.json | 2 + foundations/core/packages/core/lang/ru.json | 2 + foundations/core/packages/core/lang/tr.json | 2 + foundations/core/packages/core/lang/zh.json | 2 + foundations/core/packages/core/src/classes.ts | 1 + .../core/packages/core/src/component.ts | 4 +- .../middleware/src/spacePermissions.ts | 50 +++++++++++++++---- .../components/navigator/CreateSpace.svelte | 22 +++++--- 16 files changed, 84 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e3c954517c4..ee2409e1d15 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -97,7 +97,7 @@ "MODEL_JSON": "${workspaceRoot}/models/all/bundle/model.json", // "SERVER_PROVIDER":"uweb" "SERVER_PROVIDER": "ws", - "MODEL_VERSION": "0.7.301", + "MODEL_VERSION": "0.7.312", // "version": "0.7.0", "COMMUNICATION_API_ENABLED": "true", "ELASTIC_INDEX_NAME": "local_storage_index", diff --git a/foundations/core/packages/core/lang/cs.json b/foundations/core/packages/core/lang/cs.json index 8a312b656ee..3c9d294dded 100644 --- a/foundations/core/packages/core/lang/cs.json +++ b/foundations/core/packages/core/lang/cs.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor", "AutoJoin": "Automatické připojení", "AutoJoinDescr": "Automaticky připojit nové zaměstnance k tomuto prostoru", + "RBAC": "Řízení přístupu na základě rolí", + "RBACDescr": "Vyžadovat řízení přístupu na základě rolí pro provádění akcí v tomto prostoru", "BlobSize": "Velikost", "BlobContentType": "Typ obsahu", "Relation": "Vztah", diff --git a/foundations/core/packages/core/lang/de.json b/foundations/core/packages/core/lang/de.json index bc8768fa84b..882a2bae142 100644 --- a/foundations/core/packages/core/lang/de.json +++ b/foundations/core/packages/core/lang/de.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren", "AutoJoin": "Automatisch beitreten", "AutoJoinDescr": "Neue Mitarbeiter automatisch diesem Arbeitsbereich hinzufügen", + "RBAC": "Rollenbasierte Zugriffskontrolle", + "RBACDescr": "Erfordert rollenbasierten Zugriff, um Aktionen in diesem Arbeitsbereich auszuführen", "BlobSize": "Größe", "BlobContentType": "Inhaltstyp", "Relation": "Beziehung", diff --git a/foundations/core/packages/core/lang/en.json b/foundations/core/packages/core/lang/en.json index f9687e31593..a264b7816e7 100644 --- a/foundations/core/packages/core/lang/en.json +++ b/foundations/core/packages/core/lang/en.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Grants users ability to archive the space", "AutoJoin": "Auto join", "AutoJoinDescr": "Automatically join new employees to this space", + "RBAC": "Role-based access control", + "RBACDescr": "Require role-based access to perform actions in this space", "BlobSize": "Size", "BlobContentType": "Content type", "Relation": "Relation", diff --git a/foundations/core/packages/core/lang/es.json b/foundations/core/packages/core/lang/es.json index e6917fa2810..33209ce9a9a 100644 --- a/foundations/core/packages/core/lang/es.json +++ b/foundations/core/packages/core/lang/es.json @@ -54,6 +54,8 @@ "ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio", "AutoJoin": "Auto unirse", "AutoJoinDescr": "Unirse automáticamente a los nuevos empleados a este espacio", + "RBAC": "Control de acceso basado en roles", + "RBACDescr": "Requerir acceso basado en roles para realizar acciones en este espacio", "BlobSize": "Tamaño", "BlobContentType": "Tipo de contenido", "Relation": "Relación", diff --git a/foundations/core/packages/core/lang/fr.json b/foundations/core/packages/core/lang/fr.json index bb4b311b710..8a812efa598 100644 --- a/foundations/core/packages/core/lang/fr.json +++ b/foundations/core/packages/core/lang/fr.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace", "AutoJoin": "Rejoindre automatiquement", "AutoJoinDescr": "Ajouter automatiquement les nouveaux employés à cet espace", + "RBAC": "Contrôle d'accès basé sur les rôles", + "RBACDescr": "Exiger un accès basé sur les rôles pour effectuer des actions dans cet espace", "BlobSize": "Taille", "BlobContentType": "Type de contenu", "Relation": "Relation", diff --git a/foundations/core/packages/core/lang/it.json b/foundations/core/packages/core/lang/it.json index 68f072205e3..d88b56773d1 100644 --- a/foundations/core/packages/core/lang/it.json +++ b/foundations/core/packages/core/lang/it.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio", "AutoJoin": "Partecipazione automatica", "AutoJoinDescr": "Aggiungi automaticamente i nuovi dipendenti a questo spazio", + "RBAC": "Controllo accessi basato sui ruoli", + "RBACDescr": "Richiedi accesso basato sui ruoli per eseguire azioni in questo spazio", "BlobSize": "Dimensione", "BlobContentType": "Tipo di contenuto", "Relation": "Relazione", diff --git a/foundations/core/packages/core/lang/ja.json b/foundations/core/packages/core/lang/ja.json index cdb6c0daa11..8a7c323f045 100644 --- a/foundations/core/packages/core/lang/ja.json +++ b/foundations/core/packages/core/lang/ja.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します", "AutoJoin": "自動参加", "AutoJoinDescr": "新しいユーザーを自動的にこのスペースに参加させます", + "RBAC": "ロールベースアクセス制御", + "RBACDescr": "このスペースでの操作を実行するためにロールベースのアクセスを要求します", "BlobSize": "サイズ", "BlobContentType": "コンテンツタイプ", "Relation": "関係", diff --git a/foundations/core/packages/core/lang/pt.json b/foundations/core/packages/core/lang/pt.json index c7df192c7d5..5ea2afb74ef 100644 --- a/foundations/core/packages/core/lang/pt.json +++ b/foundations/core/packages/core/lang/pt.json @@ -54,6 +54,8 @@ "ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço", "AutoJoin": "Auto adesão", "AutoJoinDescr": "Adesão automática de novos funcionários a este espaço", + "RBAC": "Controle de acesso baseado em funções", + "RBACDescr": "Exigir acesso baseado em funções para executar ações neste espaço", "BlobSize": "Tamanho", "BlobContentType": "Tipo de conteúdo", "Relation": "Relação", diff --git a/foundations/core/packages/core/lang/ru.json b/foundations/core/packages/core/lang/ru.json index 24cb255c36b..bac8eed1c4a 100644 --- a/foundations/core/packages/core/lang/ru.json +++ b/foundations/core/packages/core/lang/ru.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство", "AutoJoin": "Автоприсоединение", "AutoJoinDescr": "Автоматически присоединять новых сотрудников к этому пространству", + "RBAC": "Ролевой доступ", + "RBACDescr": "Требовать ролевой доступ для выполнения действий в этом пространстве", "BlobSize": "Размер", "BlobContentType": "Тип контента", "Relation": "Связь", diff --git a/foundations/core/packages/core/lang/tr.json b/foundations/core/packages/core/lang/tr.json index 1f862d2e085..4d14fc39ae8 100644 --- a/foundations/core/packages/core/lang/tr.json +++ b/foundations/core/packages/core/lang/tr.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir", "AutoJoin": "Otomatik katıl", "AutoJoinDescr": "Yeni çalışanları bu alana otomatik olarak ekle", + "RBAC": "Rol tabanlı erişim kontrolü", + "RBACDescr": "Bu alanda işlemleri gerçekleştirmek için rol tabanlı erişim gerektir", "BlobSize": "Boyut", "BlobContentType": "İçerik türü", "Relation": "İlişki", diff --git a/foundations/core/packages/core/lang/zh.json b/foundations/core/packages/core/lang/zh.json index 696a334f60e..21bab655b18 100644 --- a/foundations/core/packages/core/lang/zh.json +++ b/foundations/core/packages/core/lang/zh.json @@ -61,6 +61,8 @@ "ArchiveSpaceDescription": "授予用户归档空间的权限", "AutoJoin": "自动加入", "AutoJoinDescr": "自动将新员工加入此空间", + "RBAC": "基于角色的访问控制", + "RBACDescr": "要求基于角色的访问权限以在此空间中执行操作", "BlobSize": "大小", "BlobContentType": "內容類型", "Relation": "关系", diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts index 359e49e681d..c4ec2518a6f 100644 --- a/foundations/core/packages/core/src/classes.ts +++ b/foundations/core/packages/core/src/classes.ts @@ -490,6 +490,7 @@ export interface SystemSpace extends Space {} */ export interface TypedSpace extends Space { type: Ref + restricted?: boolean // if true user must have permission to any txes } /** diff --git a/foundations/core/packages/core/src/component.ts b/foundations/core/packages/core/src/component.ts index 439ab5d8275..1a2364c2846 100644 --- a/foundations/core/packages/core/src/component.ts +++ b/foundations/core/packages/core/src/component.ts @@ -289,7 +289,9 @@ export default plugin(coreId, { UpdateSpaceDescription: '' as IntlString, ArchiveSpaceDescription: '' as IntlString, AutoJoin: '' as IntlString, - AutoJoinDescr: '' as IntlString + AutoJoinDescr: '' as IntlString, + RBAC: '' as IntlString, + RBACDescr: '' as IntlString }, descriptor: { SpacesType: '' as Ref diff --git a/foundations/server/packages/middleware/src/spacePermissions.ts b/foundations/server/packages/middleware/src/spacePermissions.ts index 1a0b1c56b98..9174663d76b 100644 --- a/foundations/server/packages/middleware/src/spacePermissions.ts +++ b/foundations/server/packages/middleware/src/spacePermissions.ts @@ -45,6 +45,7 @@ import { BaseMiddleware } from '@hcengineering/server-core' */ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middleware { private whitelistSpaces = new Set>() + private readonly restrictedSpaces = new Set>() private assignmentBySpace: Record, RolesAssignment> = {} private permissionsBySpace: Record, Record>> = {} private typeBySpace: Record, Ref> = {} @@ -121,6 +122,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle return } + if (space.restricted === true) { + this.restrictedSpaces.add(space._id) + } else { + this.restrictedSpaces.delete(space._id) + } + this.typeBySpace[space._id] = space.type const asMixin: RolesAssignment = this.context.hierarchy.as( @@ -157,7 +164,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle * * Checks if the required permission is present in the space for the given context */ - private checkPermission (ctx: MeasureContext, space: Ref, tx: TxCUD): boolean { + private checkPermission ( + ctx: MeasureContext, + space: Ref, + tx: TxCUD, + isSpace: boolean + ): boolean { const account = ctx.contextData.account const permissions = this.permissionsBySpace[space]?.[account.uuid] ?? [] let withoutMatch: Permission | undefined @@ -185,7 +197,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle return withoutMatch.forbid !== undefined ? !withoutMatch.forbid : true } - return true + return isSpace || !this.restrictedSpaces.has(space) } private throwForbidden (): void { @@ -273,6 +285,7 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle delete this.typeBySpace[tx.objectId] this.whitelistSpaces.delete(tx.objectId) + this.restrictedSpaces.delete(tx.objectId) } private isSpaceTxCUD (tx: TxCUD): tx is TxCUD { @@ -337,9 +350,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle if (this.isSpaceTxCUD(tx)) { if (tx._class === core.class.TxCreateDoc) { this.handleCreate(tx) - // } else if (tx._class === core.class.TxUpdateDoc) { - // Roles assignment in spaces are managed through the space type mixin - // so nothing to handle here + } else if (tx._class === core.class.TxUpdateDoc) { + this.handleSpaceUpdate(tx) } else if (tx._class === core.class.TxMixin) { this.handleMixin(tx) } else if (tx._class === core.class.TxRemoveDoc) { @@ -350,6 +362,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle this.handlePermissionsUpdatesFromRoleTx(ctx, tx) } + private handleSpaceUpdate (tx: TxCUD): void { + if (!this.isTypedSpaceClass(tx.objectClass)) { + return + } + + const updateTx = tx as TxUpdateDoc + if (updateTx.operations.restricted !== undefined) { + if (updateTx.operations.restricted) { + this.restrictedSpaces.add(tx.objectId) + } else { + this.restrictedSpaces.delete(tx.objectId) + } + } + } + private processPermissionsUpdatesFromTx (ctx: MeasureContext, tx: Tx): void { if (!TxProcessor.isExtendsCUD(tx._class)) { return @@ -362,8 +389,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle async tx (ctx: MeasureContext, txes: Tx[]): Promise { await this.init(ctx) for (const tx of txes) { - this.processPermissionsUpdatesFromTx(ctx, tx) this.checkPermissions(ctx, tx) + this.processPermissionsUpdatesFromTx(ctx, tx) } const res = await this.provideTx(ctx, txes) for (const txd of ctx.contextData.broadcast.txes) { @@ -388,17 +415,22 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace) if (isSpace) { - this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref) + this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref, true) } } - private checkSpacePermissions (ctx: MeasureContext, cudTx: TxCUD, targetSpaceId: Ref): void { + private checkSpacePermissions ( + ctx: MeasureContext, + cudTx: TxCUD, + targetSpaceId: Ref, + isSpace: boolean = false + ): void { if (this.whitelistSpaces.has(targetSpaceId)) { return } // NOTE: move this checking logic later to be defined in some server plugins? // so they can contribute checks into the middleware for their custom permissions? - if (!this.checkPermission(ctx, targetSpaceId as Ref, cudTx)) { + if (!this.checkPermission(ctx, targetSpaceId as Ref, cudTx, isSpace)) { this.throwForbidden() } } diff --git a/plugins/card-resources/src/components/navigator/CreateSpace.svelte b/plugins/card-resources/src/components/navigator/CreateSpace.svelte index 286cae42c1e..7987c856731 100644 --- a/plugins/card-resources/src/components/navigator/CreateSpace.svelte +++ b/plugins/card-resources/src/components/navigator/CreateSpace.svelte @@ -39,12 +39,13 @@ let types: Ref[] = space?.types !== undefined ? hierarchy.clone(space.types) : topLevelTypes.map((it) => it._id) - let roles = client.getModel().findAllSync(card.class.Role, { type: { $in: types } }) - $: roles = client.getModel().findAllSync(card.class.Role, { type: { $in: types } }) + let roles = client.getModel().findAllSync(card.class.Role, { types: { $in: types } }) + $: roles = client.getModel().findAllSync(card.class.Role, { types: { $in: types } }) let name: string = space?.name ?? '' let isPrivate: boolean = space?.private ?? false + let restricted: boolean = space?.restricted ?? false let members: AccountUuid[] = space?.members !== undefined ? hierarchy.clone(space.members) : [getCurrentAccount().uuid] let owners: AccountUuid[] = space?.owners !== undefined ? hierarchy.clone(space.owners) : [getCurrentAccount().uuid] @@ -86,7 +87,8 @@ autoJoin, archived: false, type: card.spaceType.SpaceType, - types + types, + restricted } } @@ -106,9 +108,9 @@ } async function create (): Promise { - const teamspaceData = getData() + const data = getData() - const id = await client.createDoc(card.class.CardSpace, core.space.Space, { ...teamspaceData }) + const id = await client.createDoc(card.class.CardSpace, core.space.Space, data) if (rolesAssignment && !deepEqual(rolesAssignment, getRolesAssignment(roles))) { await client.updateMixin(id, card.class.CardSpace, core.space.Space, core.mixin.SpacesTypeData, rolesAssignment) @@ -221,7 +223,15 @@