Table attribute editors should respect rbac (#10275)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-12-10 19:02:51 +05:00
committed by GitHub
parent 83e110f4ab
commit 669f05a094
10 changed files with 182 additions and 72 deletions
@@ -15,7 +15,7 @@
<script lang="ts">
import { Card } from '@hcengineering/card'
import { PermissionsStore } from '@hcengineering/contact'
import { canChangeAttribute, permissionsStore } from '@hcengineering/contact-resources'
import { permissionsStore } from '@hcengineering/contact-resources'
import core, { AnyAttribute, Class, Doc, Ref, toRank, TypedSpace } from '@hcengineering/core'
import {
AttributeBarEditor,
@@ -25,6 +25,7 @@
isCollectionAttr,
KeyedAttribute
} from '@hcengineering/presentation'
import { canChangeAttribute } from '@hcengineering/view-resources'
export let object: Card
export let _class: Ref<Class<Doc>>
@@ -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<Card>
export let readonly: boolean = false
@@ -32,7 +32,13 @@
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 +48,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<Card>
export let readonly: boolean = false
@@ -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
-53
View File
@@ -53,7 +53,6 @@ import {
import core, {
type AccountUuid,
type AggregateValue,
type AnyAttribute,
type Class,
type Client,
type Doc,
@@ -671,58 +670,6 @@ export function checkMyPermission (_id: Ref<Permission>, space: Ref<TypedSpace>,
return (store.whitelist.has(space) || store.ps[space]?.has(_id)) ?? false
}
export function canChangeAttribute (
attr: AnyAttribute,
space: Ref<TypedSpace>,
store: PermissionsStore,
_class?: Ref<Class<Doc>>
): 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<Permission>
const forbidden = store.ps[space]?.has(forbiddenId)
if (forbidden) {
return false
}
const allowedId = `${attr._id}_allowed` as Ref<Permission>
const allowed = store.ps[space]?.has(allowedId)
if (allowed) {
return true
}
return canChangeDoc(_class ?? attr.attributeOf, space, store)
}
export function canChangeDoc (_class: Ref<Class<Doc>>, space: Ref<Space>, 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)
}
export function getPermittedPersons (
_id: Ref<Permission>,
space: Ref<TypedSpace>,
@@ -13,15 +13,19 @@
// limitations under the License.
-->
<script lang="ts">
import core, { AnyAttribute, Class, Doc, Ref, TxOperations, getObjectValue } from '@hcengineering/core'
import contact, { PermissionsStore } from '@hcengineering/contact'
import core, { AnyAttribute, Class, Doc, Ref, TxOperations, TypedSpace, getObjectValue } from '@hcengineering/core'
import { getClient, reduceCalls, updateAttribute } from '@hcengineering/presentation'
import { Label, Loading, mouseAttractor, resizeObserver } from '@hcengineering/ui'
import { Label, Loading, mouseAttractor } from '@hcengineering/ui'
import { AttributeModel, BuildModelKey, BuildModelOptions, Viewlet } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onMount } from 'svelte'
import { showMenu } from '../actions'
import { buildModel, restrictionStore } from '../utils'
import view from '../plugin'
import { buildModel, restrictionStore } from '../utils'
import { getResource } from '@hcengineering/platform'
import { Readable } from 'svelte/store'
import { canChangeAttribute } from '../permissions'
export let objects: Doc[]
export let config: Array<string | BuildModelKey>
@@ -150,6 +154,22 @@
}
}
}
let permissionsStore: Readable<PermissionsStore> | undefined = undefined
onMount(async () => {
permissionsStore = await getResource(contact.store.Permissions)
})
function canChangeAttr (
object: Doc,
attr: AnyAttribute | undefined,
permissionsStore: PermissionsStore | undefined
): boolean {
if (attr === undefined) return true
if (permissionsStore === undefined) return true
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
}
</script>
{#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)
)}
/>
</div>
</td>
@@ -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<Class<Doc>>
export let query: DocumentQuery<Doc>
@@ -338,6 +343,22 @@
}
}
let permissionsStore: Readable<PermissionsStore> | 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<TypedSpace>, permissionsStore)
}
async function canEdit (object: Doc): Promise<boolean> {
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
)}
/>
</div>
{: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}
</td>
@@ -13,10 +13,13 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import core, { Doc } from '@hcengineering/core'
import contact, { PermissionsStore } from '@hcengineering/contact'
import core, { AnyAttribute, Doc, Ref, TypedSpace } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { AttributeModel } from '@hcengineering/view'
import { FixedColumn, restrictionStore } from '../..'
import { createEventDispatcher, onMount } from 'svelte'
import { Readable } from 'svelte/store'
import { canChangeAttribute, FixedColumn, restrictionStore } from '../..'
import DividerPresenter from './DividerPresenter.svelte'
export let docObject: Doc
@@ -51,6 +54,22 @@
if (e.detail === undefined) return
dispatch('resize', e.detail)
}
let permissionsStore: Readable<PermissionsStore> | undefined = undefined
onMount(async () => {
permissionsStore = await getResource(contact.store.Permissions)
})
function canChangeAttr (
object: Doc,
attr: AnyAttribute | undefined,
permissionsStore: PermissionsStore | undefined
): boolean {
if (attr === undefined) return true
if (permissionsStore === undefined) return true
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore)
}
</script>
{#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}
/>
</FixedColumn>
@@ -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}
+1
View File
@@ -199,6 +199,7 @@ export * from './objectIterator'
export * from './selection'
export * from './status'
export * from './utils'
export * from './permissions'
export {
buildModel,
getActiveViewletId,
+64
View File
@@ -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<TypedSpace>,
store: PermissionsStore,
_class?: Ref<Class<Doc>>
): 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<Permission>
const forbidden = store.ps[space]?.has(forbiddenId)
if (forbidden) {
return false
}
const allowedId = `${attr._id}_allowed` as Ref<Permission>
const allowed = store.ps[space]?.has(allowedId)
if (allowed) {
return true
}
return canChangeDoc(_class ?? attr.attributeOf, space, store)
}
export function canChangeDoc (_class: Ref<Class<Doc>>, space: Ref<Space>, 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)
}