From 6269ddd37a359f8a2469511b57b6984908c0eac8 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Sun, 18 Oct 2026 05:29:44 +0000 Subject: [PATCH] feat(core): add Forbid create object and Forbid edit object space permissions Space type roles could only forbid deleting objects. Add two more space-scoped forbid permissions, 'Forbid create object' (TxCreateDoc) and 'Forbid edit object' (TxUpdateDoc, which also covers mixin attribute updates), and offer them in all space types that already offer 'Forbid delete object'. They are enforced server-side by SpacePermissionsMiddleware like the existing forbid permission and apply to all objects inside the space, including comments and attachments. Two adjustments in the middleware: - these generic create/update forbids are not applied to the space document itself, so role holders can still manage members, role assignments and archiving of the space; - when several matching permissions without txMatch apply, a forbidding one now wins instead of depending on the role/permission order. On the client, canCreateObject and canChangeDoc (used by inline attribute editors, drive and card) honour the new permissions. Fixes #9079 Signed-off-by: Michael Uray --- foundations/core/packages/core/lang/cs.json | 4 + foundations/core/packages/core/lang/de.json | 4 + foundations/core/packages/core/lang/en.json | 4 + foundations/core/packages/core/lang/es.json | 4 + foundations/core/packages/core/lang/fr.json | 4 + foundations/core/packages/core/lang/it.json | 4 + foundations/core/packages/core/lang/ja.json | 4 + foundations/core/packages/core/lang/ko.json | 4 + foundations/core/packages/core/lang/pl.json | 4 + .../core/packages/core/lang/pt-br.json | 4 + foundations/core/packages/core/lang/pt.json | 4 + foundations/core/packages/core/lang/ru.json | 4 + foundations/core/packages/core/lang/tr.json | 4 + foundations/core/packages/core/lang/zh.json | 4 + .../core/packages/core/src/component.ts | 6 + .../middleware/src/spacePermissions.ts | 19 +- .../src/tests/spacePermissions.test.ts | 219 ++++++++++++++++++ models/board/src/index.ts | 4 +- models/core/src/permissions.ts | 26 +++ models/document/src/index.ts | 4 +- models/drive/src/index.ts | 4 +- models/lead/src/spaceType.ts | 4 +- models/recruit/src/spaceType.ts | 7 +- models/test-management/src/index.ts | 4 +- models/tracker/src/index.ts | 4 +- plugins/view-resources/src/permissions.ts | 6 + services/github/model-github/src/index.ts | 6 +- 27 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 foundations/server/packages/middleware/src/tests/spacePermissions.test.ts diff --git a/foundations/core/packages/core/lang/cs.json b/foundations/core/packages/core/lang/cs.json index e9f035cd82d..df8b7a6cb6b 100644 --- a/foundations/core/packages/core/lang/cs.json +++ b/foundations/core/packages/core/lang/cs.json @@ -51,12 +51,16 @@ "UpdateObject": "Aktualizovat objekt", "DeleteObject": "Smazat objekt", "ForbidDeleteObject": "Zakázat mazání objektu", + "ForbidCreateObject": "Zakázat vytváření objektu", + "ForbidUpdateObject": "Zakázat úpravu objektu", "UpdateSpace": "Aktualizovat prostor", "ArchiveSpace": "Archivovat prostor", "CreateObjectDescription": "Umožňuje uživatelům vytvářet objekty v prostoru", "UpdateObjectDescription": "Umožňuje uživatelům aktualizovat objekty v prostoru", "DeleteObjectDescription": "Umožňuje uživatelům mazat objekty v prostoru", "ForbidDeleteObjectDescription": "Zakazuje uživatelům mazání objektů v prostoru", + "ForbidCreateObjectDescription": "Zakazuje uživatelům vytváření objektů v prostoru", + "ForbidUpdateObjectDescription": "Zakazuje uživatelům úpravu objektů v prostoru", "UpdateSpaceDescription": "Umožňuje uživatelům aktualizovat prostor", "ArchiveSpaceDescription": "Umožňuje uživatelům archivovat prostor", "AutoJoin": "Automatické připojení", diff --git a/foundations/core/packages/core/lang/de.json b/foundations/core/packages/core/lang/de.json index 57272652b5e..e8c4d3e47f8 100644 --- a/foundations/core/packages/core/lang/de.json +++ b/foundations/core/packages/core/lang/de.json @@ -51,12 +51,16 @@ "UpdateObject": "Objekt aktualisieren", "DeleteObject": "Objekt löschen", "ForbidDeleteObject": "Objekt löschen verbieten", + "ForbidCreateObject": "Objekt erstellen verbieten", + "ForbidUpdateObject": "Objekt bearbeiten verbieten", "UpdateSpace": "Arbeitsbereich aktualisieren", "ArchiveSpace": "Arbeitsbereich archivieren", "CreateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu erstellen", "UpdateObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu aktualisieren", "DeleteObjectDescription": "Gewährt Benutzern die Möglichkeit, Objekte im Arbeitsbereich zu löschen", "ForbidDeleteObjectDescription": "Verbietet Benutzern das Löschen von Objekten im Arbeitsbereich", + "ForbidCreateObjectDescription": "Verbietet Benutzern das Erstellen von Objekten im Arbeitsbereich", + "ForbidUpdateObjectDescription": "Verbietet Benutzern das Bearbeiten von Objekten im Arbeitsbereich", "UpdateSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu aktualisieren", "ArchiveSpaceDescription": "Gewährt Benutzern die Möglichkeit, den Arbeitsbereich zu archivieren", "AutoJoin": "Automatisch beitreten", diff --git a/foundations/core/packages/core/lang/en.json b/foundations/core/packages/core/lang/en.json index aacdb84a17c..aecec7263e8 100644 --- a/foundations/core/packages/core/lang/en.json +++ b/foundations/core/packages/core/lang/en.json @@ -51,12 +51,16 @@ "UpdateObject": "Update object", "DeleteObject": "Delete object", "ForbidDeleteObject": "Forbid delete object", + "ForbidCreateObject": "Forbid create object", + "ForbidUpdateObject": "Forbid edit object", "UpdateSpace": "Update space", "ArchiveSpace": "Archive space", "CreateObjectDescription": "Grants users ability to create objects in the space", "UpdateObjectDescription": "Grants users ability to update objects in the space", "DeleteObjectDescription": "Grants users ability to delete objects in the space", "ForbidDeleteObjectDescription": "Forbid users deleting objects in the space", + "ForbidCreateObjectDescription": "Forbid users creating objects in the space", + "ForbidUpdateObjectDescription": "Forbid users editing objects in the space", "UpdateSpaceDescription": "Grants users ability to update the space", "ArchiveSpaceDescription": "Grants users ability to archive the space", "AutoJoin": "Auto join", diff --git a/foundations/core/packages/core/lang/es.json b/foundations/core/packages/core/lang/es.json index ff1867cea67..1c71a63510f 100644 --- a/foundations/core/packages/core/lang/es.json +++ b/foundations/core/packages/core/lang/es.json @@ -44,12 +44,16 @@ "UpdateObject": "Actualizar objeto", "DeleteObject": "Eliminar objeto", "ForbidDeleteObject": "Prohibir eliminar objeto", + "ForbidCreateObject": "Prohibir crear objeto", + "ForbidUpdateObject": "Prohibir editar objeto", "UpdateSpace": "Actualizar espacio", "ArchiveSpace": "Archivar espacio", "CreateObjectDescription": "Concede a los usuarios la capacidad de crear objetos en el espacio", "UpdateObjectDescription": "Concede a los usuarios la capacidad de actualizar objetos en el espacio", "DeleteObjectDescription": "Concede a los usuarios la capacidad de eliminar objetos en el espacio", "ForbidDeleteObjectDescription": "Prohíbe a los usuarios eliminar objetos en el espacio", + "ForbidCreateObjectDescription": "Prohíbe a los usuarios crear objetos en el espacio", + "ForbidUpdateObjectDescription": "Prohíbe a los usuarios editar objetos en el espacio", "UpdateSpaceDescription": "Concede a los usuarios la capacidad de actualizar el espacio", "ArchiveSpaceDescription": "Concede a los usuarios la capacidad de archivar el espacio", "AutoJoin": "Auto unirse", diff --git a/foundations/core/packages/core/lang/fr.json b/foundations/core/packages/core/lang/fr.json index 9e6e354a0e6..cad12701f46 100644 --- a/foundations/core/packages/core/lang/fr.json +++ b/foundations/core/packages/core/lang/fr.json @@ -51,12 +51,16 @@ "UpdateObject": "Mettre à jour l'objet", "DeleteObject": "Supprimer l'objet", "ForbidDeleteObject": "Interdire la suppression de l'objet", + "ForbidCreateObject": "Interdire la création de l'objet", + "ForbidUpdateObject": "Interdire la modification de l'objet", "UpdateSpace": "Mettre à jour l'espace", "ArchiveSpace": "Archiver l'espace", "CreateObjectDescription": "Accorde aux utilisateurs la capacité de créer des objets dans l'espace", "UpdateObjectDescription": "Accorde aux utilisateurs la capacité de mettre à jour les objets dans l'espace", "DeleteObjectDescription": "Accorde aux utilisateurs la capacité de supprimer des objets dans l'espace", "ForbidDeleteObjectDescription": "Interdire aux utilisateurs de supprimer des objets dans l'espace", + "ForbidCreateObjectDescription": "Interdire aux utilisateurs de créer des objets dans l'espace", + "ForbidUpdateObjectDescription": "Interdire aux utilisateurs de modifier des objets dans l'espace", "UpdateSpaceDescription": "Accorde aux utilisateurs la capacité de mettre à jour l'espace", "ArchiveSpaceDescription": "Accorde aux utilisateurs la capacité d'archiver l'espace", "AutoJoin": "Rejoindre automatiquement", diff --git a/foundations/core/packages/core/lang/it.json b/foundations/core/packages/core/lang/it.json index 86794b5ef16..4e6b292e885 100644 --- a/foundations/core/packages/core/lang/it.json +++ b/foundations/core/packages/core/lang/it.json @@ -51,12 +51,16 @@ "UpdateObject": "Aggiorna oggetto", "DeleteObject": "Elimina oggetto", "ForbidDeleteObject": "Proibisci eliminazione oggetto", + "ForbidCreateObject": "Proibisci creazione oggetto", + "ForbidUpdateObject": "Proibisci modifica oggetto", "UpdateSpace": "Aggiorna spazio", "ArchiveSpace": "Archivia spazio", "CreateObjectDescription": "Concede agli utenti la possibilità di creare oggetti nello spazio", "UpdateObjectDescription": "Concede agli utenti la possibilità di aggiornare oggetti nello spazio", "DeleteObjectDescription": "Concede agli utenti la possibilità di eliminare oggetti nello spazio", "ForbidDeleteObjectDescription": "Proibisce agli utenti di eliminare oggetti nello spazio", + "ForbidCreateObjectDescription": "Proibisce agli utenti di creare oggetti nello spazio", + "ForbidUpdateObjectDescription": "Proibisce agli utenti di modificare oggetti nello spazio", "UpdateSpaceDescription": "Concede agli utenti la possibilità di aggiornare lo spazio", "ArchiveSpaceDescription": "Concede agli utenti la possibilità di archiviare lo spazio", "AutoJoin": "Partecipazione automatica", diff --git a/foundations/core/packages/core/lang/ja.json b/foundations/core/packages/core/lang/ja.json index d71787fe1d9..ff816e380f6 100644 --- a/foundations/core/packages/core/lang/ja.json +++ b/foundations/core/packages/core/lang/ja.json @@ -51,12 +51,16 @@ "UpdateObject": "オブジェクトの更新", "DeleteObject": "オブジェクトの削除", "ForbidDeleteObject": "オブジェクトの削除禁止", + "ForbidCreateObject": "オブジェクトの作成禁止", + "ForbidUpdateObject": "オブジェクトの編集禁止", "UpdateSpace": "スペースの更新", "ArchiveSpace": "スペースのアーカイブ", "CreateObjectDescription": "ユーザーにスペース内でオブジェクトを作成する権限を付与します", "UpdateObjectDescription": "ユーザーにスペース内でオブジェクトを更新する権限を付与します", "DeleteObjectDescription": "ユーザーにスペース内でオブジェクトを削除する権限を付与します", "ForbidDeleteObjectDescription": "ユーザーによるスペース内のオブジェクトの削除を禁止します", + "ForbidCreateObjectDescription": "ユーザーによるスペース内のオブジェクトの作成を禁止します", + "ForbidUpdateObjectDescription": "ユーザーによるスペース内のオブジェクトの編集を禁止します", "UpdateSpaceDescription": "ユーザーにスペースを更新する権限を付与します", "ArchiveSpaceDescription": "ユーザーにスペースをアーカイブする権限を付与します", "AutoJoin": "自動参加", diff --git a/foundations/core/packages/core/lang/ko.json b/foundations/core/packages/core/lang/ko.json index 5985ffba610..83d7f68d1a1 100644 --- a/foundations/core/packages/core/lang/ko.json +++ b/foundations/core/packages/core/lang/ko.json @@ -51,12 +51,16 @@ "UpdateObject": "객체 업데이트", "DeleteObject": "객체 삭제", "ForbidDeleteObject": "객체 삭제 금지", + "ForbidCreateObject": "객체 생성 금지", + "ForbidUpdateObject": "객체 편집 금지", "UpdateSpace": "스페이스 업데이트", "ArchiveSpace": "스페이스 보관", "CreateObjectDescription": "스페이스에서 객체를 만들 수 있는 권한을 부여합니다", "UpdateObjectDescription": "스페이스에서 객체를 업데이트할 수 있는 권한을 부여합니다", "DeleteObjectDescription": "스페이스에서 객체를 삭제할 수 있는 권한을 부여합니다", "ForbidDeleteObjectDescription": "스페이스에서 객체 삭제를 금지합니다", + "ForbidCreateObjectDescription": "스페이스에서 객체 생성을 금지합니다", + "ForbidUpdateObjectDescription": "스페이스에서 객체 편집을 금지합니다", "UpdateSpaceDescription": "스페이스를 업데이트할 수 있는 권한을 부여합니다", "ArchiveSpaceDescription": "스페이스를 보관할 수 있는 권한을 부여합니다", "AutoJoin": "자동 참여", diff --git a/foundations/core/packages/core/lang/pl.json b/foundations/core/packages/core/lang/pl.json index 95a4f0f1fb1..f84adc8cf7f 100644 --- a/foundations/core/packages/core/lang/pl.json +++ b/foundations/core/packages/core/lang/pl.json @@ -51,12 +51,16 @@ "UpdateObject": "Zaktualizuj przedmiot", "DeleteObject": "Usuń przedmiot", "ForbidDeleteObject": "Zabroń usunięcia przedmiotu", + "ForbidCreateObject": "Zabroń utworzenia przedmiotu", + "ForbidUpdateObject": "Zabroń edycji przedmiotu", "UpdateSpace": "Zaktualizuj przestrzeń", "ArchiveSpace": "Archiwizuj przestrzeń", "CreateObjectDescription": "Pozwala użytkownikom tworzyć obiekty w przestrzeni", "UpdateObjectDescription": "Pozwala użytkownikom aktualizować obiekty w przestrzeni", "DeleteObjectDescription": "Pozwala użytkownikom usuwać obiekty w przestrzeni", "ForbidDeleteObjectDescription": "Uniemożliwia użytkownikom usuwania obiektów w przestrzeni", + "ForbidCreateObjectDescription": "Uniemożliwia użytkownikom tworzenie obiektów w przestrzeni", + "ForbidUpdateObjectDescription": "Uniemożliwia użytkownikom edycję obiektów w przestrzeni", "UpdateSpaceDescription": "Pozwala użytkownikom aktualizować przestrzeń", "ArchiveSpaceDescription": "Pozwala użytkownikom archiwizować przestrzeń", "AutoJoin": "Dołącz automatycznie", diff --git a/foundations/core/packages/core/lang/pt-br.json b/foundations/core/packages/core/lang/pt-br.json index 6c00b6811c3..89e2a8d8229 100644 --- a/foundations/core/packages/core/lang/pt-br.json +++ b/foundations/core/packages/core/lang/pt-br.json @@ -44,12 +44,16 @@ "UpdateObject": "Atualizar objeto", "DeleteObject": "Apagar objeto", "ForbidDeleteObject": "Proibir apagar objeto", + "ForbidCreateObject": "Proibir criar objeto", + "ForbidUpdateObject": "Proibir editar objeto", "UpdateSpace": "Atualizar espaço", "ArchiveSpace": "Arquivar espaço", "CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço", "UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço", "DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço", "ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço", + "ForbidCreateObjectDescription": "Proíbe aos usuários a capacidade de criar objetos no espaço", + "ForbidUpdateObjectDescription": "Proíbe aos usuários a capacidade de editar objetos no espaço", "UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço", "ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço", "AutoJoin": "Auto adesão", diff --git a/foundations/core/packages/core/lang/pt.json b/foundations/core/packages/core/lang/pt.json index befce7c131b..9f7bbca1625 100644 --- a/foundations/core/packages/core/lang/pt.json +++ b/foundations/core/packages/core/lang/pt.json @@ -44,12 +44,16 @@ "UpdateObject": "Atualizar objeto", "DeleteObject": "Apagar objeto", "ForbidDeleteObject": "Proibir apagar objeto", + "ForbidCreateObject": "Proibir criar objeto", + "ForbidUpdateObject": "Proibir editar objeto", "UpdateSpace": "Atualizar espaço", "ArchiveSpace": "Arquivar espaço", "CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço", "UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço", "DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço", "ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço", + "ForbidCreateObjectDescription": "Proíbe aos utilizadores a capacidade de criar objetos no espaço", + "ForbidUpdateObjectDescription": "Proíbe aos utilizadores a capacidade de editar objetos no espaço", "UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço", "ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço", "AutoJoin": "Auto adesão", diff --git a/foundations/core/packages/core/lang/ru.json b/foundations/core/packages/core/lang/ru.json index 1d72f0e94b7..2999f25ba96 100644 --- a/foundations/core/packages/core/lang/ru.json +++ b/foundations/core/packages/core/lang/ru.json @@ -51,12 +51,16 @@ "UpdateObject": "Обновлять объект", "DeleteObject": "Удалять объект", "ForbidDeleteObject": "Запретить удалять объект", + "ForbidCreateObject": "Запретить создавать объект", + "ForbidUpdateObject": "Запретить редактировать объект", "UpdateSpace": "Обновлять пространство", "ArchiveSpace": "Архивировать пространство", "CreateObjectDescription": "Дает пользователям разрешение создавать объекты в пространстве", "UpdateObjectDescription": "Дает пользователям разрешение обновлять объекты в пространстве", "DeleteObjectDescription": "Дает пользователям разрешение удалять объекты в пространстве", "ForbidDeleteObjectDescription": "Запрещает пользователям удалять объекты в пространстве", + "ForbidCreateObjectDescription": "Запрещает пользователям создавать объекты в пространстве", + "ForbidUpdateObjectDescription": "Запрещает пользователям редактировать объекты в пространстве", "UpdateSpaceDescription": "Дает пользователям разрешение обновлять пространство", "ArchiveSpaceDescription": "Дает пользователям разрешение архивировать пространство", "AutoJoin": "Автоприсоединение", diff --git a/foundations/core/packages/core/lang/tr.json b/foundations/core/packages/core/lang/tr.json index d6f6ce068c6..d68707b0a77 100644 --- a/foundations/core/packages/core/lang/tr.json +++ b/foundations/core/packages/core/lang/tr.json @@ -51,12 +51,16 @@ "UpdateObject": "Nesneyi güncelle", "DeleteObject": "Nesneyi sil", "ForbidDeleteObject": "Nesne silmeyi yasakla", + "ForbidCreateObject": "Nesne oluşturmayı yasakla", + "ForbidUpdateObject": "Nesne düzenlemeyi yasakla", "UpdateSpace": "Alanı güncelle", "ArchiveSpace": "Alanı arşivle", "CreateObjectDescription": "Kullanıcılara alanda nesne oluşturma yetkisi verir", "UpdateObjectDescription": "Kullanıcılara alandaki nesneleri güncelleme yetkisi verir", "DeleteObjectDescription": "Kullanıcılara alandaki nesneleri silme yetkisi verir", "ForbidDeleteObjectDescription": "Kullanıcıların alandaki nesneleri silmesini yasaklar", + "ForbidCreateObjectDescription": "Kullanıcıların alanda nesne oluşturmasını yasaklar", + "ForbidUpdateObjectDescription": "Kullanıcıların alandaki nesneleri düzenlemesini yasaklar", "UpdateSpaceDescription": "Kullanıcılara alanı güncelleme yetkisi verir", "ArchiveSpaceDescription": "Kullanıcılara alanı arşivleme yetkisi verir", "AutoJoin": "Otomatik katıl", diff --git a/foundations/core/packages/core/lang/zh.json b/foundations/core/packages/core/lang/zh.json index dd55ddf5ef4..f1ce613fe19 100644 --- a/foundations/core/packages/core/lang/zh.json +++ b/foundations/core/packages/core/lang/zh.json @@ -51,12 +51,16 @@ "UpdateObject": "更新对象", "DeleteObject": "删除对象", "ForbidDeleteObject": "禁止删除对象", + "ForbidCreateObject": "禁止创建对象", + "ForbidUpdateObject": "禁止编辑对象", "UpdateSpace": "更新空间", "ArchiveSpace": "归档空间", "CreateObjectDescription": "授予用户在空间中创建对象的权限", "UpdateObjectDescription": "授予用户在空间中更新对象的权限", "DeleteObjectDescription": "授予用户在空间中删除对象的权限", "ForbidDeleteObjectDescription": "禁止用户在空间中删除对象", + "ForbidCreateObjectDescription": "禁止用户在空间中创建对象", + "ForbidUpdateObjectDescription": "禁止用户在空间中编辑对象", "UpdateSpaceDescription": "授予用户更新空间的权限", "ArchiveSpaceDescription": "授予用户归档空间的权限", "AutoJoin": "自动加入", diff --git a/foundations/core/packages/core/src/component.ts b/foundations/core/packages/core/src/component.ts index 0fd395cf6e1..5614d1bdf5e 100644 --- a/foundations/core/packages/core/src/component.ts +++ b/foundations/core/packages/core/src/component.ts @@ -289,12 +289,16 @@ export default plugin(coreId, { UpdateObject: '' as IntlString, DeleteObject: '' as IntlString, ForbidDeleteObject: '' as IntlString, + ForbidCreateObject: '' as IntlString, + ForbidUpdateObject: '' as IntlString, UpdateSpace: '' as IntlString, ArchiveSpace: '' as IntlString, CreateObjectDescription: '' as IntlString, UpdateObjectDescription: '' as IntlString, DeleteObjectDescription: '' as IntlString, ForbidDeleteObjectDescription: '' as IntlString, + ForbidCreateObjectDescription: '' as IntlString, + ForbidUpdateObjectDescription: '' as IntlString, UpdateSpaceDescription: '' as IntlString, ArchiveSpaceDescription: '' as IntlString, AutoJoin: '' as IntlString, @@ -316,6 +320,8 @@ export default plugin(coreId, { UpdateObject: '' as Ref, DeleteObject: '' as Ref, ForbidDeleteObject: '' as Ref, + ForbidCreateObject: '' as Ref, + ForbidUpdateObject: '' as Ref, UpdateSpace: '' as Ref, ArchiveSpace: '' as Ref }, diff --git a/foundations/server/packages/middleware/src/spacePermissions.ts b/foundations/server/packages/middleware/src/spacePermissions.ts index 6acd7f1634e..d7dff4e55fe 100644 --- a/foundations/server/packages/middleware/src/spacePermissions.ts +++ b/foundations/server/packages/middleware/src/spacePermissions.ts @@ -182,8 +182,12 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle ) { continue } + if (isSpace && isObjectOnlyPermission(permission)) continue if (permission.txMatch === undefined) { - withoutMatch = permission + // A forbidding permission wins over an allowing one, independent of the role/permission order + if (withoutMatch === undefined || permission.forbid === true) { + withoutMatch = permission + } continue } else { const checkMatch = matchQuery([tx], permission.txMatch, tx._class, this.context.hierarchy, true) @@ -490,6 +494,19 @@ function isTxClassMatched (tx: Tx, permission: Permission): boolean { return false } +/** + * Generic (no objectClass) forbid create/update permissions restrict objects inside a space, + * but not the space document itself: otherwise the role holders could not manage the space + * (members, roles assignment, archiving) anymore. + */ +function isObjectOnlyPermission (permission: Permission): boolean { + return ( + permission.forbid === true && + permission.objectClass === undefined && + (permission.txClass === core.class.TxCreateDoc || permission.txClass === core.class.TxUpdateDoc) + ) +} + function getTxObjectClass (tx: TxCUD): Ref> { return tx._class === core.class.TxMixin ? (tx as TxMixin).mixin : tx.objectClass } diff --git a/foundations/server/packages/middleware/src/tests/spacePermissions.test.ts b/foundations/server/packages/middleware/src/tests/spacePermissions.test.ts new file mode 100644 index 00000000000..c59028e71c2 --- /dev/null +++ b/foundations/server/packages/middleware/src/tests/spacePermissions.test.ts @@ -0,0 +1,219 @@ +// +// Copyright © 2026 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, { + AccountRole, + MeasureMetricsContext, + type Account, + type AccountUuid, + type Class, + type Doc, + type MeasureContext, + type Permission, + type PersonId, + type Ref, + type Role, + type SessionData, + type Space, + type SpaceType, + type Tx, + type TxCUD, + TxFactory, + type TypedSpace +} from '@hcengineering/core' +import type { PipelineContext } from '@hcengineering/server-core' +import { SpacePermissionsMiddleware } from '../spacePermissions' + +const SPACE_CLASS = 'test:class:Project' as Ref> +const OBJECT_CLASS = 'test:class:Issue' as Ref> +const OBJECT_MIXIN = 'test:mixin:IssueMixin' as Ref> +const TYPE_MIXIN = 'test:mixin:ProjectTypeData' as Ref> +const SPACE_TYPE = 'test:spaceType:Project' as Ref +const SPACE = 'test:space:Project' as Ref +const ROLE = 'test:role:Reader' as Ref +const ALLOW_UPDATE_ISSUE = 'test:permission:UpdateIssue' as Ref +const CLASS_ALLOWED = 'test:class:Issue_allowed' as Ref + +const member = 'member-uuid' as AccountUuid +const other = 'other-uuid' as AccountUuid + +function makePermission (_id: Ref, data: Partial): Permission { + return { + _id, + _class: core.class.Permission, + space: core.space.Model, + modifiedOn: 0, + modifiedBy: core.account.System, + label: '' as any, + scope: 'space', + ...data + } +} + +const permissions: Permission[] = [ + makePermission(core.permission.ForbidCreateObject, { txClass: core.class.TxCreateDoc, forbid: true }), + makePermission(core.permission.ForbidUpdateObject, { txClass: core.class.TxUpdateDoc, forbid: true }), + makePermission(core.permission.ForbidDeleteObject, { txClass: core.class.TxRemoveDoc, forbid: true }), + makePermission(ALLOW_UPDATE_ISSUE, { txClass: core.class.TxUpdateDoc, objectClass: OBJECT_CLASS as any }), + makePermission(CLASS_ALLOWED, { + txClass: core.class.TxUpdateDoc, + objectClass: OBJECT_CLASS as any, + txMatch: { objectClass: OBJECT_CLASS }, + forbid: false + }) +] + +function makeMiddleware (rolePermissions: Array>): SpacePermissionsMiddleware { + const role: Role = { + _id: ROLE, + _class: core.class.Role, + space: core.space.Model, + attachedTo: SPACE_TYPE, + name: 'Reader', + permissions: rolePermissions + } as any + const spaceType: SpaceType = { + _id: SPACE_TYPE, + _class: core.class.SpaceType, + space: core.space.Model, + targetClass: TYPE_MIXIN + } as any + const space: TypedSpace = { + _id: SPACE, + _class: SPACE_CLASS, + space: core.space.Space, + type: SPACE_TYPE, + members: [member, other], + [ROLE]: [member] + } as any + + const hierarchy = { + isDerived: (_class: Ref>, base: Ref>) => + _class === base || + (_class === SPACE_CLASS && (base === core.class.Space || base === core.class.TypedSpace)) || + (_class === TYPE_MIXIN && base === core.class.Space), + getAncestors: (_class: Ref>) => [_class], + getBaseClass: (_class: Ref>) => _class, + findAttribute: () => undefined, + as: (doc: Doc) => doc + } + const modelDb = { + findAllSync: (_class: Ref>, query: any) => { + if (_class === core.class.SpaceType) return [spaceType] + if (_class === core.class.Role) return [role] + if (_class === core.class.Permission) return query?.objectClass !== undefined ? [] : permissions + return [] + } + } + const context: PipelineContext = { hierarchy, modelDb } as any + const next = { + findAll: async (_ctx: MeasureContext, _class: Ref>) => (_class === core.class.Space ? [space] : []), + tx: async () => ({}) + } + return new (SpacePermissionsMiddleware as any)(context, next) +} + +function makeCtx (uuid: AccountUuid, system = false): MeasureContext { + const account: Account = { + uuid, + role: AccountRole.User, + primarySocialId: system ? core.account.System : ('social' as PersonId), + socialIds: [], + fullSocialIds: [] + } + const ctx = new MeasureMetricsContext('test', {}) as MeasureContext + ctx.contextData = { account, broadcast: { txes: [], queue: [], sessions: {} } } as any + return ctx +} + +const factory = new TxFactory('social' as PersonId) +const objectId = 'test:issue:1' as Ref + +const createTx = (): TxCUD => factory.createTxCreateDoc(OBJECT_CLASS, SPACE, { title: 'x' } as any) +const updateTx = (): TxCUD => factory.createTxUpdateDoc(OBJECT_CLASS, SPACE, objectId, { title: 'y' } as any) +const mixinTx = (): TxCUD => + factory.createTxMixin(objectId, OBJECT_CLASS, SPACE, OBJECT_MIXIN, { field: 1 } as any) +const removeTx = (): TxCUD => factory.createTxRemoveDoc(OBJECT_CLASS, SPACE, objectId) +const spaceUpdateTx = (): TxCUD => + factory.createTxUpdateDoc(SPACE_CLASS, core.space.Space, SPACE, { archived: true } as any) +const spaceRolesTx = (): TxCUD => + factory.createTxMixin(SPACE, SPACE_CLASS, core.space.Space, TYPE_MIXIN, { [ROLE]: [member] } as any) +const spaceRemoveTx = (): TxCUD => factory.createTxRemoveDoc(SPACE_CLASS, core.space.Space, SPACE) + +async function isAllowed (mw: SpacePermissionsMiddleware, tx: Tx, uuid: AccountUuid = member): Promise { + try { + await mw.tx(makeCtx(uuid), [tx]) + return true + } catch (err: any) { + return false + } +} + +describe('SpacePermissionsMiddleware forbid create/update object', () => { + it('ForbidCreateObject blocks creating objects in the space only', async () => { + const mw = makeMiddleware([core.permission.ForbidCreateObject]) + expect(await isAllowed(mw, createTx())).toBe(false) + expect(await isAllowed(mw, updateTx())).toBe(true) + expect(await isAllowed(mw, removeTx())).toBe(true) + }) + + it('ForbidUpdateObject blocks updates and mixin updates of objects in the space', async () => { + const mw = makeMiddleware([core.permission.ForbidUpdateObject]) + expect(await isAllowed(mw, updateTx())).toBe(false) + expect(await isAllowed(mw, mixinTx())).toBe(false) + expect(await isAllowed(mw, createTx())).toBe(true) + expect(await isAllowed(mw, removeTx())).toBe(true) + }) + + it('does not restrict members without the role', async () => { + const mw = makeMiddleware([core.permission.ForbidCreateObject, core.permission.ForbidUpdateObject]) + expect(await isAllowed(mw, createTx(), other)).toBe(true) + expect(await isAllowed(mw, updateTx(), other)).toBe(true) + }) + + it('does not restrict the system account', async () => { + const mw = makeMiddleware([core.permission.ForbidCreateObject, core.permission.ForbidUpdateObject]) + await expect(mw.tx(makeCtx(member, true), [createTx(), updateTx()])).resolves.toBeDefined() + }) + + it('checks transactions inside TxApplyIf', async () => { + const mw = makeMiddleware([core.permission.ForbidUpdateObject]) + const apply = factory.createTxApplyIf(core.space.Workspace, 'scope', [], [], [updateTx()], 'test') + expect(await isAllowed(mw, apply)).toBe(false) + }) + + it('does not lock role holders out of managing the space itself', async () => { + const mw = makeMiddleware([core.permission.ForbidCreateObject, core.permission.ForbidUpdateObject]) + expect(await isAllowed(mw, spaceUpdateTx())).toBe(true) + expect(await isAllowed(mw, spaceRolesTx())).toBe(true) + }) + + it('keeps ForbidDeleteObject applying to the space itself', async () => { + const mw = makeMiddleware([core.permission.ForbidDeleteObject]) + expect(await isAllowed(mw, spaceRemoveTx())).toBe(false) + }) + + it('forbid wins over an allowing permission regardless of order', async () => { + const mw1 = makeMiddleware([ALLOW_UPDATE_ISSUE, core.permission.ForbidUpdateObject]) + expect(await isAllowed(mw1, updateTx())).toBe(false) + const mw2 = makeMiddleware([core.permission.ForbidUpdateObject, ALLOW_UPDATE_ISSUE]) + expect(await isAllowed(mw2, updateTx())).toBe(false) + }) + + it('a class/attribute permission with txMatch takes precedence over the generic forbid', async () => { + const mw = makeMiddleware([core.permission.ForbidUpdateObject, CLASS_ALLOWED]) + expect(await isAllowed(mw, updateTx())).toBe(true) + }) +}) diff --git a/models/board/src/index.ts b/models/board/src/index.ts index 36054241d80..1e1674a3730 100644 --- a/models/board/src/index.ts +++ b/models/board/src/index.ts @@ -508,7 +508,9 @@ export function createModel (builder: Builder): void { availablePermissions: [ core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ], allowedTaskTypeDescriptors: [board.descriptors.Card] }, diff --git a/models/core/src/permissions.ts b/models/core/src/permissions.ts index 3c731d1f3d7..f0619bec20d 100644 --- a/models/core/src/permissions.ts +++ b/models/core/src/permissions.ts @@ -64,6 +64,32 @@ export function definePermissions (builder: Builder): void { core.permission.ForbidDeleteObject ) + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.ForbidCreateObject, + txClass: core.class.TxCreateDoc, + forbid: true, + scope: 'space', + description: core.string.ForbidCreateObjectDescription + }, + core.permission.ForbidCreateObject + ) + + builder.createDoc( + core.class.Permission, + core.space.Model, + { + label: core.string.ForbidUpdateObject, + txClass: core.class.TxUpdateDoc, + forbid: true, + scope: 'space', + description: core.string.ForbidUpdateObjectDescription + }, + core.permission.ForbidUpdateObject + ) + builder.createDoc( core.class.Permission, core.space.Model, diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 380a7efffd7..dcbf0d2046c 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -167,7 +167,9 @@ function defineTeamspace (builder: Builder): void { availablePermissions: [ core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ] }, document.descriptor.TeamspaceType diff --git a/models/drive/src/index.ts b/models/drive/src/index.ts index 35b23ed18d4..5a18d853a37 100644 --- a/models/drive/src/index.ts +++ b/models/drive/src/index.ts @@ -242,7 +242,9 @@ function defineDrive (builder: Builder): void { drive.permission.RemoveFile, core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ] }, drive.descriptor.DriveType diff --git a/models/lead/src/spaceType.ts b/models/lead/src/spaceType.ts index 633e966a9ae..24eb4a18d35 100644 --- a/models/lead/src/spaceType.ts +++ b/models/lead/src/spaceType.ts @@ -91,7 +91,9 @@ export function defineSpaceType (builder: Builder): void { availablePermissions: [ core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ], allowedTaskTypeDescriptors: [plugin.descriptors.Lead] }, diff --git a/models/recruit/src/spaceType.ts b/models/recruit/src/spaceType.ts index a4df714da78..b7c4357c7b3 100644 --- a/models/recruit/src/spaceType.ts +++ b/models/recruit/src/spaceType.ts @@ -82,7 +82,12 @@ export function defineSpaceType (builder: Builder): void { icon: plugin.icon.RecruitApplication, editor: plugin.component.VacancyTemplateEditor, baseClass: plugin.class.Vacancy, - availablePermissions: [core.permission.ArchiveSpace, core.permission.ForbidDeleteObject], + availablePermissions: [ + core.permission.ArchiveSpace, + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject + ], allowedTaskTypeDescriptors: [plugin.descriptors.Application] }, plugin.descriptors.VacancyType diff --git a/models/test-management/src/index.ts b/models/test-management/src/index.ts index 610e3b47981..93af9a65df9 100644 --- a/models/test-management/src/index.ts +++ b/models/test-management/src/index.ts @@ -270,7 +270,9 @@ function defineSpaceType (builder: Builder): void { availablePermissions: [ core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ] }, testManagement.descriptors.ProjectType diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index e785d304098..7f6089fa078 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -847,7 +847,9 @@ function defineSpaceType (builder: Builder): void { availablePermissions: [ core.permission.UpdateSpace, core.permission.ArchiveSpace, - core.permission.ForbidDeleteObject + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject ], allowedClassic: true, allowedTaskTypeDescriptors: [tracker.descriptors.Issue] diff --git a/plugins/view-resources/src/permissions.ts b/plugins/view-resources/src/permissions.ts index c1c368ca45c..5ece4ae21c7 100644 --- a/plugins/view-resources/src/permissions.ts +++ b/plugins/view-resources/src/permissions.ts @@ -61,6 +61,9 @@ export function canChangeDoc (_class: Ref>, space: Ref, store: if (allowedCl) { return true } + if (store.ps[space]?.has(core.permission.ForbidUpdateObject)) { + return false + } const client = getClient() const h = client.getHierarchy() const ancestors = h.getAncestors(_class) @@ -112,6 +115,9 @@ export function canCreateObject (_class: Ref>, space: Ref, sto if (arePermissionsDisabled) return true if (store.whitelist.has(space)) return true if (store.ps[space] !== undefined) { + if (store.ps[space]?.has(core.permission.ForbidCreateObject)) { + return false + } const client = getClient() const h = client.getHierarchy() const ancestors = h.getAncestors(_class) diff --git a/services/github/model-github/src/index.ts b/services/github/model-github/src/index.ts index 84978141444..645ce36cead 100644 --- a/services/github/model-github/src/index.ts +++ b/services/github/model-github/src/index.ts @@ -940,7 +940,11 @@ export function createModel (builder: Builder): void { description: github.string.GithubDesc, icon: github.icon.Github, name: github.string.Github, - availablePermissions: [core.permission.ForbidDeleteObject] + availablePermissions: [ + core.permission.ForbidDeleteObject, + core.permission.ForbidCreateObject, + core.permission.ForbidUpdateObject + ] }, github.descriptors.GithubProject )