From d6f6b0451a202b885868ed355a9267e5ab395663 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 3 Dec 2025 21:50:48 +0500 Subject: [PATCH 1/3] Table attribute editors should respect rbac Signed-off-by: Denis Bykhov --- .../src/components/CardAttributes.svelte | 3 +- plugins/contact-resources/src/utils.ts | 53 --------------- .../src/components/DocTable.svelte | 36 +++++++++-- .../src/components/Table.svelte | 41 +++++++++++- .../src/components/list/ListPresenter.svelte | 39 +++++++++-- plugins/view-resources/src/index.ts | 1 + plugins/view-resources/src/permissions.ts | 64 +++++++++++++++++++ 7 files changed, 170 insertions(+), 67 deletions(-) create mode 100644 plugins/view-resources/src/permissions.ts diff --git a/plugins/card-resources/src/components/CardAttributes.svelte b/plugins/card-resources/src/components/CardAttributes.svelte index de71079ac26..c6c8e97b20c 100644 --- a/plugins/card-resources/src/components/CardAttributes.svelte +++ b/plugins/card-resources/src/components/CardAttributes.svelte @@ -15,7 +15,7 @@ {#if !model || isBuildingModel} @@ -206,7 +226,13 @@ onChange={getOnChange(object, attribute)} label={attribute.label} attribute={attribute.attribute} - {...joinProps(attribute, object, readonly || $restrictionStore.readonly)} + {...joinProps( + attribute, + object, + readonly || + $restrictionStore.readonly || + !canChangeAttr(object, attribute.attribute, $permissionsStore) + )} /> diff --git a/plugins/view-resources/src/components/Table.svelte b/plugins/view-resources/src/components/Table.svelte index 64c511bc70a..98613303b72 100644 --- a/plugins/view-resources/src/components/Table.svelte +++ b/plugins/view-resources/src/components/Table.svelte @@ -25,6 +25,7 @@ Ref, SortingOrder, TxOperations, + TypedSpace, getObjectValue, mergeQueries } from '@hcengineering/core' @@ -43,13 +44,17 @@ } from '@hcengineering/ui' import { AttributeModel, BuildModelKey, BuildModelOptions, ViewOptionModel, ViewOptions } from '@hcengineering/view' import { deepEqual } from 'fast-equals' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, onMount } from 'svelte' import { showMenu } from '../actions' import view from '../plugin' import { LoadingProps, buildConfigAssociation, buildConfigLookup, buildModel, restrictionStore } from '../utils' import IconUpDown from './icons/UpDown.svelte' import { getResultOptions, getResultQuery } from '../viewOptions' import { canEditSpace } from '../visibilityTester' + import contact, { PermissionsStore } from '@hcengineering/contact' + import { Readable } from 'svelte/store' + import { getResource } from '@hcengineering/platform' + import { canChangeAttribute } from '../permissions' export let _class: Ref> export let query: DocumentQuery @@ -338,6 +343,22 @@ } } + let permissionsStore: Readable | undefined = undefined + + onMount(async () => { + permissionsStore = await getResource(contact.store.Permissions) + }) + + function canChangeAttr ( + object: Doc, + attr: AnyAttribute | undefined, + permissionsStore: PermissionsStore | undefined + ): boolean { + if (permissionsStore === undefined) return true + if (attr === undefined) return true + return canChangeAttribute(attr, object.space as Ref, permissionsStore) + } + async function canEdit (object: Doc): Promise { if (client.getHierarchy().isDerived(object._class, core.class.Space)) { return await canEditSpace(object) @@ -474,7 +495,14 @@ onChange={getOnChange(object, attribute)} label={attribute.label} attribute={attribute.attribute} - {...joinProps(attribute, object, readonly || $restrictionStore.readonly, canEditObject)} + {...joinProps( + attribute, + object, + readonly || + $restrictionStore.readonly || + !canChangeAttr(object, attribute.attribute, $permissionsStore), + canEditObject + )} /> {:else} @@ -484,7 +512,14 @@ onChange={getOnChange(object, attribute)} label={attribute.label} attribute={attribute.attribute} - {...joinProps(attribute, object, readonly || $restrictionStore.readonly, canEditObject)} + {...joinProps( + attribute, + object, + readonly || + $restrictionStore.readonly || + !canChangeAttr(object, attribute.attribute, $permissionsStore), + canEditObject + )} /> {/if} diff --git a/plugins/view-resources/src/components/list/ListPresenter.svelte b/plugins/view-resources/src/components/list/ListPresenter.svelte index 098842757e7..978b6666140 100644 --- a/plugins/view-resources/src/components/list/ListPresenter.svelte +++ b/plugins/view-resources/src/components/list/ListPresenter.svelte @@ -13,10 +13,13 @@ // limitations under the License. --> {#if dp?.dividerBefore === true && !hideDivider} @@ -66,7 +85,12 @@ {compactMode} label={attributeModel.label} attribute={attributeModel.attribute} - {...joinProps(attributeModel, docObject, props, $restrictionStore.readonly)} + {...joinProps( + attributeModel, + docObject, + props, + $restrictionStore.readonly || !canChangeAttr(docObject, attributeModel.attribute, $permissionsStore) + )} on:resize={translateSize} /> @@ -79,7 +103,12 @@ label={attributeModel.label} {compactMode} attribute={attributeModel.attribute} - {...joinProps(attributeModel, docObject, props, $restrictionStore.readonly)} + {...joinProps( + attributeModel, + docObject, + props, + $restrictionStore.readonly || !canChangeAttr(docObject, attributeModel.attribute, $permissionsStore) + )} on:resize={translateSize} /> {/if} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 3c2571c8d47..012d4fdad33 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -199,6 +199,7 @@ export * from './objectIterator' export * from './selection' export * from './status' export * from './utils' +export * from './permissions' export { buildModel, getActiveViewletId, diff --git a/plugins/view-resources/src/permissions.ts b/plugins/view-resources/src/permissions.ts new file mode 100644 index 00000000000..203df5a1ce8 --- /dev/null +++ b/plugins/view-resources/src/permissions.ts @@ -0,0 +1,64 @@ +import { type PermissionsStore } from '@hcengineering/contact' +import core, { + type AnyAttribute, + type Class, + type Doc, + type Permission, + type Ref, + type Space, + type TypedSpace +} from '@hcengineering/core' +import { getMetadata } from '@hcengineering/platform' +import { getClient } from '@hcengineering/presentation' + +export function canChangeAttribute ( + attr: AnyAttribute, + space: Ref, + store: PermissionsStore, + _class?: Ref> +): boolean { + const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false + if (arePermissionsDisabled) return true + if (store.whitelist.has(space)) return true + const forbiddenId = `${attr._id}_forbidden` as Ref + const forbidden = store.ps[space]?.has(forbiddenId) + if (forbidden) { + return false + } + const allowedId = `${attr._id}_allowed` as Ref + const allowed = store.ps[space]?.has(allowedId) + if (allowed) { + return true + } + + return canChangeDoc(_class ?? attr.attributeOf, space, store) +} + +export function canChangeDoc (_class: Ref>, space: Ref, store: PermissionsStore): boolean { + const arePermissionsDisabled = getMetadata(core.metadata.DisablePermissions) ?? false + if (arePermissionsDisabled) return true + if (store.whitelist.has(space)) return true + if (store.ps[space] !== undefined) { + const client = getClient() + const h = client.getHierarchy() + const ancestors = h.getAncestors(_class) + const permissions = client + .getModel() + .findAllSync(core.class.Permission, { txClass: { $in: [core.class.TxUpdateDoc, core.class.TxMixin] } }) + for (const ancestor of ancestors) { + const curr = permissions.filter( + (p) => + p.objectClass === ancestor && + p.txMatch === undefined && + p.txClass === (h.isMixin(ancestor) ? core.class.TxMixin : core.class.TxUpdateDoc) + ) + for (const permission of curr) { + if (store.ps[space]?.has(permission._id)) { + return permission.forbid !== true + } + } + } + } + + return !store.restrictedSpaces.has(space) +} From 8db96eddde1266919ddae1b80548ce73dbe1b8ae Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 10 Dec 2025 17:15:15 +0500 Subject: [PATCH 2/3] Fix Signed-off-by: Denis Bykhov --- plugins/card-resources/src/components/EditCardNew.svelte | 4 ++-- plugins/card-resources/src/components/EditCardOld.svelte | 4 ++-- .../src/components/sections/ContentSection.svelte | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/card-resources/src/components/EditCardNew.svelte b/plugins/card-resources/src/components/EditCardNew.svelte index 837686acd72..3540e87d724 100644 --- a/plugins/card-resources/src/components/EditCardNew.svelte +++ b/plugins/card-resources/src/components/EditCardNew.svelte @@ -38,7 +38,7 @@ getClient, ComponentExtensions } from '@hcengineering/presentation' - import { showMenu } from '@hcengineering/view-resources' + import { canChangeDoc, showMenu } from '@hcengineering/view-resources' import view from '@hcengineering/view' import { NotificationContext } from '@hcengineering/communication-types' @@ -49,7 +49,7 @@ import ParentNamesPresenter from './ParentNamesPresenter.svelte' import { openCardInSidebar } from '../utils' import { afterUpdate } from 'svelte' - import { canChangeDoc, permissionsStore } from '@hcengineering/contact-resources' + import { permissionsStore } from '@hcengineering/contact-resources' export let _id: Ref export let readonly: boolean = false diff --git a/plugins/card-resources/src/components/EditCardOld.svelte b/plugins/card-resources/src/components/EditCardOld.svelte index 0b34b4ba4b7..ddacb35a191 100644 --- a/plugins/card-resources/src/components/EditCardOld.svelte +++ b/plugins/card-resources/src/components/EditCardOld.svelte @@ -32,7 +32,7 @@ navigate } from '@hcengineering/ui' import view from '@hcengineering/view' - import { ParentsNavigator, RelationsEditor, getDocMixins, showMenu } from '@hcengineering/view-resources' + import { ParentsNavigator, RelationsEditor, canChangeDoc, getDocMixins, showMenu } from '@hcengineering/view-resources' import { createEventDispatcher, onDestroy } from 'svelte' import card from '../plugin' @@ -42,7 +42,7 @@ import Childs from './Childs.svelte' import Content from './Content.svelte' import TagsEditor from './TagsEditor.svelte' - import { canChangeDoc, permissionsStore } from '@hcengineering/contact-resources' + import { permissionsStore } from '@hcengineering/contact-resources' export let _id: Ref export let readonly: boolean = false diff --git a/plugins/card-resources/src/components/sections/ContentSection.svelte b/plugins/card-resources/src/components/sections/ContentSection.svelte index 856b01819b1..d264a1a02aa 100644 --- a/plugins/card-resources/src/components/sections/ContentSection.svelte +++ b/plugins/card-resources/src/components/sections/ContentSection.svelte @@ -20,7 +20,8 @@ import Content from '../Content.svelte' import { CardSectionAction } from '../../types' - import { canChangeDoc, permissionsStore } from '@hcengineering/contact-resources' + import { permissionsStore } from '@hcengineering/contact-resources' + import { canChangeDoc } from '@hcengineering/view-resources' export let readonly: boolean = false export let doc: Card From f8b4587ce256e2fdcb1193c413df31698093927b Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 10 Dec 2025 17:37:26 +0500 Subject: [PATCH 3/3] Fix formatting Signed-off-by: Denis Bykhov --- plugins/card-resources/src/components/EditCardOld.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/card-resources/src/components/EditCardOld.svelte b/plugins/card-resources/src/components/EditCardOld.svelte index ddacb35a191..90a44216d94 100644 --- a/plugins/card-resources/src/components/EditCardOld.svelte +++ b/plugins/card-resources/src/components/EditCardOld.svelte @@ -32,7 +32,13 @@ navigate } from '@hcengineering/ui' import view from '@hcengineering/view' - import { ParentsNavigator, RelationsEditor, canChangeDoc, getDocMixins, showMenu } from '@hcengineering/view-resources' + import { + ParentsNavigator, + RelationsEditor, + canChangeDoc, + getDocMixins, + showMenu + } from '@hcengineering/view-resources' import { createEventDispatcher, onDestroy } from 'svelte' import card from '../plugin'