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) +}