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