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 )