@conduction/nextcloud-vue 2.119.0 → 2.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
- package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
- package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
- package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
- package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
- package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
- package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
- package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
- package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
- package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
- package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
- package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +116 -16
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +207 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +207 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +951 -16
- package/dist/esm/utils/aiFeatures.js +245 -0
- package/dist/esm/utils/aiFeatures.js.map +1 -0
- package/dist/esm/utils/aiSuggestionTracker.js +182 -0
- package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectPermissions.js +552 -0
- package/dist/esm/utils/objectPermissions.js.map +1 -0
- package/dist/esm/utils/objectTranslations.js +180 -0
- package/dist/esm/utils/objectTranslations.js.map +1 -0
- package/dist/esm/utils/stateMachineValidation.js +254 -0
- package/dist/esm/utils/stateMachineValidation.js.map +1 -0
- package/dist/esm/utils/widgetDispatch.js +3 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +23038 -14567
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +951 -16
- package/l10n/en.json +207 -1
- package/l10n/nl.json +207 -1
- package/package.json +1 -1
- package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
- package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
- package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
- package/src/components/CnAiFeaturesWidget/index.js +14 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
- package/src/components/CnIndexPage/useNamedSource.js +9 -0
- package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
- package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
- package/src/components/CnObjectPermissionsTab/index.js +13 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
- package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
- package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
- package/src/components/CnObjectTranslationsWidget/index.js +13 -0
- package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
- package/src/components/CnReadinessChecklist/index.js +9 -0
- package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
- package/src/components/CnSchemaRulesWidget/index.js +10 -0
- package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
- package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
- package/src/components/CnSearchIndexStatus/index.js +10 -0
- package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
- package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
- package/src/components/CnShippedConfigurationSection/index.js +9 -0
- package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
- package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
- package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
- package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
- package/src/components/CnStateMachineEditor/index.js +8 -0
- package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
- package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
- package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
- package/src/components/index.js +8 -0
- package/src/composables/indexSources.js +116 -16
- package/src/css/index-page.css +7 -0
- package/src/index.js +9 -0
- package/src/utils/aiFeatures.js +241 -0
- package/src/utils/aiSuggestionTracker.js +178 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/objectPermissions.js +548 -0
- package/src/utils/objectTranslations.js +176 -0
- package/src/utils/stateMachineValidation.js +250 -0
- package/src/utils/widgetDispatch.js +3 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
|
|
5
|
+
CnObjectPermissionsTab — who holds which right on one OpenRegister object, and
|
|
6
|
+
where each grant came from. Read-only: it renders OpenRegister's answer and
|
|
7
|
+
computes none of it. One row per rule; a deny is its own row beside the grant
|
|
8
|
+
it removes. A failed read is named, never shown as an empty list.
|
|
9
|
+
-->
|
|
10
|
+
<template>
|
|
11
|
+
<div class="cn-object-permissions-tab">
|
|
12
|
+
<h3>{{ title || t('nextcloud-vue', 'Who holds which right') }}</h3>
|
|
13
|
+
|
|
14
|
+
<div v-if="loading" class="cn-object-permissions-tab__loading">
|
|
15
|
+
<NcLoadingIcon :size="20" />
|
|
16
|
+
{{ t('nextcloud-vue', 'Reading the grants on this record') }}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<template v-else>
|
|
20
|
+
<p v-if="reviewRefused" class="cn-object-permissions-tab__unreadable">
|
|
21
|
+
{{ t('nextcloud-vue', 'You may open this record. You may not review who else can.') }}
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p v-else-if="unreadable.length > 0" class="cn-object-permissions-tab__unreadable">
|
|
25
|
+
{{ t('nextcloud-vue', 'OpenRegister could not be asked for {sources}. This list is incomplete, not empty.', { sources: unreadable.join(', ') }) }}
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p v-if="enforcement" class="cn-object-permissions-tab__enforcement">
|
|
29
|
+
{{ enforcementSentence }}
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p v-if="rows.length === 0" class="cn-object-permissions-tab__empty">
|
|
33
|
+
{{ t('nextcloud-vue', 'OpenRegister reports no rule on this record.') }}
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<table v-else class="cn-object-permissions-tab__table">
|
|
37
|
+
<thead>
|
|
38
|
+
<tr>
|
|
39
|
+
<th scope="col">
|
|
40
|
+
{{ t('nextcloud-vue', 'Holder') }}
|
|
41
|
+
</th>
|
|
42
|
+
<th scope="col">
|
|
43
|
+
{{ t('nextcloud-vue', 'Right') }}
|
|
44
|
+
</th>
|
|
45
|
+
<th scope="col">
|
|
46
|
+
{{ t('nextcloud-vue', 'Where it comes from') }}
|
|
47
|
+
</th>
|
|
48
|
+
</tr>
|
|
49
|
+
</thead>
|
|
50
|
+
<tbody>
|
|
51
|
+
<tr v-for="(row, index) in rows"
|
|
52
|
+
:key="`${row.holder}-${row.right}-${row.source}-${index}`"
|
|
53
|
+
class="cn-object-permissions-tab__row">
|
|
54
|
+
<td>{{ row.holder || t('nextcloud-vue', 'Not named') }}</td>
|
|
55
|
+
<td>
|
|
56
|
+
<span class="cn-object-permissions-tab__right">{{ row.right }}</span>
|
|
57
|
+
<span v-if="describe(row.right)" class="cn-object-permissions-tab__hint">{{ describe(row.right) }}</span>
|
|
58
|
+
<span v-if="row.declared === false" class="cn-object-permissions-tab__hint">{{ t('nextcloud-vue', 'OpenRegister does not publish this right.') }}</span>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<span class="cn-object-permissions-tab__source">{{ sourceLabel(row.source) }}</span>
|
|
62
|
+
<span v-if="roleHint(row)" class="cn-object-permissions-tab__hint">{{ roleHint(row) }}</span>
|
|
63
|
+
<span v-if="inheritedHint(row)" class="cn-object-permissions-tab__hint cn-object-permissions-tab__inherited">{{ inheritedHint(row) }}</span>
|
|
64
|
+
<span v-if="row.until" class="cn-object-permissions-tab__hint">{{ endsHint(row) }}</span>
|
|
65
|
+
<span v-if="areaHint(row)" class="cn-object-permissions-tab__hint">{{ areaHint(row) }}</span>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
|
|
71
|
+
<section v-if="historyEnabled && historyReadable" class="cn-object-permissions-tab__history">
|
|
72
|
+
<h4>{{ t('nextcloud-vue', 'Who held a right on a date') }}</h4>
|
|
73
|
+
|
|
74
|
+
<label class="cn-object-permissions-tab__moment">
|
|
75
|
+
<span>{{ t('nextcloud-vue', 'Report the rights as they stood on') }}</span>
|
|
76
|
+
<input v-model="moment"
|
|
77
|
+
type="date"
|
|
78
|
+
:max="today"
|
|
79
|
+
@change="loadMoment">
|
|
80
|
+
</label>
|
|
81
|
+
|
|
82
|
+
<p v-if="momentLoading" class="cn-object-permissions-tab__empty" role="status">
|
|
83
|
+
{{ t('nextcloud-vue', 'Reading the trail for that date') }}
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
<template v-else-if="asOf">
|
|
87
|
+
<p v-if="asOf.answered === false" class="cn-object-permissions-tab__empty">
|
|
88
|
+
{{ t('nextcloud-vue', 'The trail does not reach {date}, so nobody can be reported for it.', { date: moment }) }}
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<template v-else>
|
|
92
|
+
<p class="cn-object-permissions-tab__enforcement">
|
|
93
|
+
{{ setBySentence }}
|
|
94
|
+
</p>
|
|
95
|
+
<p v-if="asOf.changedAfterwardsBy" class="cn-object-permissions-tab__enforcement">
|
|
96
|
+
{{ changedAfterwardsSentence }}
|
|
97
|
+
</p>
|
|
98
|
+
<table class="cn-object-permissions-tab__table">
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th scope="col">
|
|
102
|
+
{{ t('nextcloud-vue', 'Holder') }}
|
|
103
|
+
</th>
|
|
104
|
+
<th scope="col">
|
|
105
|
+
{{ t('nextcloud-vue', 'Right') }}
|
|
106
|
+
</th>
|
|
107
|
+
<th scope="col">
|
|
108
|
+
{{ t('nextcloud-vue', 'Where it came from') }}
|
|
109
|
+
</th>
|
|
110
|
+
</tr>
|
|
111
|
+
</thead>
|
|
112
|
+
<tbody>
|
|
113
|
+
<tr v-for="(row, index) in asOf.rows"
|
|
114
|
+
:key="`asof-${row.holder}-${row.right}-${index}`"
|
|
115
|
+
class="cn-object-permissions-tab__row">
|
|
116
|
+
<td>{{ row.holder || t('nextcloud-vue', 'Not named') }}</td>
|
|
117
|
+
<td>{{ row.right }}</td>
|
|
118
|
+
<td>
|
|
119
|
+
<span class="cn-object-permissions-tab__source">{{ sourceLabel(row.source) }}</span>
|
|
120
|
+
<span v-if="roleHint(row)" class="cn-object-permissions-tab__hint">{{ roleHint(row) }}</span>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</template>
|
|
126
|
+
</template>
|
|
127
|
+
</section>
|
|
128
|
+
|
|
129
|
+
<section v-if="fieldRows.length" class="cn-object-permissions-tab__fields">
|
|
130
|
+
<h4>{{ t('nextcloud-vue', 'Which fields each role reads') }}</h4>
|
|
131
|
+
<p class="cn-object-permissions-tab__hint">
|
|
132
|
+
{{ t('nextcloud-vue', 'A field missing from this record is missing because of a rule below, not because it is empty.') }}
|
|
133
|
+
</p>
|
|
134
|
+
<table class="cn-object-permissions-tab__table">
|
|
135
|
+
<thead>
|
|
136
|
+
<tr>
|
|
137
|
+
<th scope="col">
|
|
138
|
+
{{ t('nextcloud-vue', 'Field') }}
|
|
139
|
+
</th>
|
|
140
|
+
<th scope="col">
|
|
141
|
+
{{ t('nextcloud-vue', 'Rule') }}
|
|
142
|
+
</th>
|
|
143
|
+
<th scope="col">
|
|
144
|
+
{{ t('nextcloud-vue', 'Who it affects') }}
|
|
145
|
+
</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody>
|
|
149
|
+
<tr v-for="(row, index) in fieldRows"
|
|
150
|
+
:key="`field-${row.field}-${row.rule}-${index}`"
|
|
151
|
+
class="cn-object-permissions-tab__row">
|
|
152
|
+
<td>{{ row.field }}</td>
|
|
153
|
+
<td>
|
|
154
|
+
{{ ruleLabels[row.rule] }}
|
|
155
|
+
<span v-if="row.appliesToMe" class="cn-object-permissions-tab__source">{{ t('nextcloud-vue', 'applies to you') }}</span>
|
|
156
|
+
</td>
|
|
157
|
+
<td>
|
|
158
|
+
{{ sentenceFor(row) }}
|
|
159
|
+
<span v-if="row.reason" class="cn-object-permissions-tab__hint">{{ row.reason }}</span>
|
|
160
|
+
</td>
|
|
161
|
+
</tr>
|
|
162
|
+
</tbody>
|
|
163
|
+
</table>
|
|
164
|
+
</section>
|
|
165
|
+
</template>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<script>
|
|
170
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
171
|
+
import { NcLoadingIcon } from '@nextcloud/vue'
|
|
172
|
+
import {
|
|
173
|
+
asOfRows,
|
|
174
|
+
DEFAULT_API_BASE,
|
|
175
|
+
fetchAccessHistory,
|
|
176
|
+
fetchCallerScope,
|
|
177
|
+
fetchDenyRules,
|
|
178
|
+
fetchFieldRoleRules,
|
|
179
|
+
fetchObjectGrants,
|
|
180
|
+
fetchObjectPermissions,
|
|
181
|
+
fetchPermissionCatalogue,
|
|
182
|
+
fetchRoleGrants,
|
|
183
|
+
fieldRoleRows,
|
|
184
|
+
grantRows,
|
|
185
|
+
roleRuleLabels,
|
|
186
|
+
roleRuleSentence,
|
|
187
|
+
} from '../../utils/objectPermissions.js'
|
|
188
|
+
|
|
189
|
+
/** OpenRegister's answer to a reader who may open the object and not review it. */
|
|
190
|
+
const REVIEW_REFUSED = 403
|
|
191
|
+
|
|
192
|
+
export default {
|
|
193
|
+
name: 'CnObjectPermissionsTab',
|
|
194
|
+
components: { NcLoadingIcon },
|
|
195
|
+
|
|
196
|
+
props: {
|
|
197
|
+
/** The object uuid (injected by CnObjectSidebar). */
|
|
198
|
+
objectId: { type: String, default: '' },
|
|
199
|
+
/** Register slug or uuid (injected by CnObjectSidebar). */
|
|
200
|
+
register: { type: String, default: '' },
|
|
201
|
+
/** Schema slug or uuid (injected by CnObjectSidebar). */
|
|
202
|
+
schema: { type: String, default: '' },
|
|
203
|
+
/** OpenRegister API root. */
|
|
204
|
+
apiBase: { type: String, default: DEFAULT_API_BASE },
|
|
205
|
+
/** Offer "who held a right on a date" when OpenRegister answers the history read. */
|
|
206
|
+
historyEnabled: { type: Boolean, default: true },
|
|
207
|
+
/**
|
|
208
|
+
* Where the per-role field rules are declared:
|
|
209
|
+
* `{ typeField, typeSchema, declarationField? }`. The object's
|
|
210
|
+
* `typeField` references a `typeSchema` object in the same register
|
|
211
|
+
* whose `declarationField` (default `fieldRoleRules`) lists the rules.
|
|
212
|
+
* `null` leaves the field-rules section out.
|
|
213
|
+
*/
|
|
214
|
+
fieldRules: { type: Object, default: null },
|
|
215
|
+
/** Heading; empty uses the translated default. */
|
|
216
|
+
title: { type: String, default: '' },
|
|
217
|
+
/**
|
|
218
|
+
* Labels per source key, overriding the defaults (e.g. `{ group: '…' }`).
|
|
219
|
+
* An unknown key renders as itself.
|
|
220
|
+
*/
|
|
221
|
+
sourceLabels: { type: Object, default: () => ({}) },
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
data() {
|
|
225
|
+
return {
|
|
226
|
+
loading: true,
|
|
227
|
+
rows: [],
|
|
228
|
+
catalogue: [],
|
|
229
|
+
enforcement: '',
|
|
230
|
+
unreadable: [],
|
|
231
|
+
reviewRefused: false,
|
|
232
|
+
historyReadable: false,
|
|
233
|
+
fieldRows: [],
|
|
234
|
+
moment: '',
|
|
235
|
+
momentLoading: false,
|
|
236
|
+
asOf: null,
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
computed: {
|
|
241
|
+
target() {
|
|
242
|
+
return { register: this.register, schema: this.schema, apiBase: this.apiBase }
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
ruleLabels() {
|
|
246
|
+
return roleRuleLabels()
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/** Today, so nobody asks the trail about a date that has not happened. */
|
|
250
|
+
today() {
|
|
251
|
+
return new Date().toISOString().slice(0, 10)
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
setBySentence() {
|
|
255
|
+
if (!this.asOf?.setBy) {
|
|
256
|
+
return t('nextcloud-vue', 'These rights stood on that date. Nobody is named as setting them.')
|
|
257
|
+
}
|
|
258
|
+
return t('nextcloud-vue', '{who} set these rights.', { who: this.asOf.setBy })
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
changedAfterwardsSentence() {
|
|
262
|
+
const change = this.asOf?.changedAfterwardsBy
|
|
263
|
+
if (!change) {
|
|
264
|
+
return ''
|
|
265
|
+
}
|
|
266
|
+
return t('nextcloud-vue', '{who} changed them afterwards, on {date}.', {
|
|
267
|
+
who: change.by || t('nextcloud-vue', 'Somebody'),
|
|
268
|
+
date: change.at || t('nextcloud-vue', 'a date the trail does not give'),
|
|
269
|
+
})
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
enforcementSentence() {
|
|
273
|
+
if (this.enforcement === 'enforcing') {
|
|
274
|
+
return t('nextcloud-vue', 'A refusal below is in force now.')
|
|
275
|
+
}
|
|
276
|
+
if (this.enforcement === 'off') {
|
|
277
|
+
return t('nextcloud-vue', 'Refusals are switched off, so no rule below removes a right.')
|
|
278
|
+
}
|
|
279
|
+
return t('nextcloud-vue', 'Refusals are staged: a rule below is recorded, and removes a right once it is switched on.')
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
watch: {
|
|
284
|
+
objectId() {
|
|
285
|
+
this.asOf = null
|
|
286
|
+
this.moment = ''
|
|
287
|
+
this.load()
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
mounted() {
|
|
292
|
+
this.load()
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
methods: {
|
|
296
|
+
t,
|
|
297
|
+
|
|
298
|
+
sentenceFor(row) {
|
|
299
|
+
return roleRuleSentence(row)
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Ask OpenRegister once and render what came back.
|
|
304
|
+
*
|
|
305
|
+
* @return {Promise<void>}
|
|
306
|
+
*/
|
|
307
|
+
async load() {
|
|
308
|
+
this.loading = true
|
|
309
|
+
const permissions = this.objectId
|
|
310
|
+
? await fetchObjectPermissions(this.target, this.objectId)
|
|
311
|
+
: { status: 0, set: null }
|
|
312
|
+
|
|
313
|
+
// A 403 is OpenRegister answering "not you", not a failed read.
|
|
314
|
+
this.reviewRefused = permissions.status === REVIEW_REFUSED
|
|
315
|
+
this.historyReadable = permissions.set !== null
|
|
316
|
+
const fallback = permissions.set === null
|
|
317
|
+
|
|
318
|
+
const [catalogue, objectGrants, roleGrants, callerScope, denyRules, fieldRules] = await Promise.all([
|
|
319
|
+
fetchPermissionCatalogue(this.target),
|
|
320
|
+
fallback && this.objectId ? fetchObjectGrants(this.target, this.objectId) : null,
|
|
321
|
+
fallback ? fetchRoleGrants(this.target) : null,
|
|
322
|
+
fetchCallerScope(this.target),
|
|
323
|
+
fallback ? fetchDenyRules(this.target) : null,
|
|
324
|
+
this.fieldRules ? fetchFieldRoleRules(this.target, this.objectId, this.fieldRules) : { decided: null, declared: null },
|
|
325
|
+
])
|
|
326
|
+
|
|
327
|
+
this.fieldRows = fieldRoleRows(fieldRules.declared, fieldRules.decided)
|
|
328
|
+
|
|
329
|
+
// Each failed read is named: an auditor reading a short list has to
|
|
330
|
+
// know which source is missing from it.
|
|
331
|
+
const missing = []
|
|
332
|
+
if (catalogue === null) {
|
|
333
|
+
missing.push(t('nextcloud-vue', 'the rights it publishes'))
|
|
334
|
+
}
|
|
335
|
+
if (permissions.set === null) {
|
|
336
|
+
if (objectGrants === null) {
|
|
337
|
+
missing.push(t('nextcloud-vue', 'the shares on this record'))
|
|
338
|
+
}
|
|
339
|
+
if (roleGrants === null) {
|
|
340
|
+
missing.push(t('nextcloud-vue', 'the roles'))
|
|
341
|
+
}
|
|
342
|
+
if (denyRules === null) {
|
|
343
|
+
missing.push(t('nextcloud-vue', 'the refusals'))
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (callerScope === null) {
|
|
347
|
+
missing.push(t('nextcloud-vue', 'your own rights'))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.catalogue = catalogue?.permissions || []
|
|
351
|
+
this.enforcement = permissions.set?.denyEnforcement || catalogue?.denyEnforcement || denyRules?.denyEnforcement || ''
|
|
352
|
+
this.unreadable = this.reviewRefused ? [] : missing
|
|
353
|
+
this.rows = grantRows({ objectPermissions: permissions.set, objectGrants, roleGrants, callerScope, denyRules })
|
|
354
|
+
this.loading = false
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Ask the trail who held a right at the end of the picked day.
|
|
359
|
+
*
|
|
360
|
+
* @return {Promise<void>}
|
|
361
|
+
*/
|
|
362
|
+
async loadMoment() {
|
|
363
|
+
if (this.moment === '' || this.objectId === '') {
|
|
364
|
+
this.asOf = null
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
this.momentLoading = true
|
|
368
|
+
// End of that day, so a grant written during it counts as standing on it.
|
|
369
|
+
const { history } = await fetchAccessHistory(this.target, this.objectId, `${this.moment}T23:59:59`)
|
|
370
|
+
this.asOf = asOfRows(history)
|
|
371
|
+
this.momentLoading = false
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
roleHint(row) {
|
|
375
|
+
if (!row?.role) {
|
|
376
|
+
return row?.detail || ''
|
|
377
|
+
}
|
|
378
|
+
return t('nextcloud-vue', 'Through the role {role}', { role: row.role })
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Which record handed this rule down, named inside a sentence, never bare.
|
|
383
|
+
*
|
|
384
|
+
* @param {object} row One row.
|
|
385
|
+
* @return {string} The sentence, or ''.
|
|
386
|
+
*/
|
|
387
|
+
inheritedHint(row) {
|
|
388
|
+
if (!row?.inheritedFrom) {
|
|
389
|
+
return ''
|
|
390
|
+
}
|
|
391
|
+
return t('nextcloud-vue', 'Granted on record {record}, which this one hangs under', { record: row.inheritedFrom })
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The end as OpenRegister wrote it: printed, never judged against a clock.
|
|
396
|
+
*
|
|
397
|
+
* @param {object} row One row.
|
|
398
|
+
* @return {string} The sentence.
|
|
399
|
+
*/
|
|
400
|
+
endsHint(row) {
|
|
401
|
+
return t('nextcloud-vue', 'Written to end on {date}', { date: row.until })
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
areaHint(row) {
|
|
405
|
+
const named = [...(row?.scopedTo?.registers || []), ...(row?.scopedTo?.schemas || [])]
|
|
406
|
+
.filter((name) => typeof name === 'string' && name !== '')
|
|
407
|
+
if (named.length === 0) {
|
|
408
|
+
return ''
|
|
409
|
+
}
|
|
410
|
+
return t('nextcloud-vue', 'Only in {areas}', { areas: named.join(', ') })
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
describe(right) {
|
|
414
|
+
const entry = this.catalogue.find((permission) => permission?.action === right)
|
|
415
|
+
return entry?.description || ''
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
sourceLabel(source) {
|
|
419
|
+
if (this.sourceLabels && this.sourceLabels[source]) {
|
|
420
|
+
return this.sourceLabels[source]
|
|
421
|
+
}
|
|
422
|
+
const labels = {
|
|
423
|
+
share: t('nextcloud-vue', 'A share on this record'),
|
|
424
|
+
role: t('nextcloud-vue', 'A role'),
|
|
425
|
+
group: t('nextcloud-vue', 'A grant on a group of types'),
|
|
426
|
+
object: t('nextcloud-vue', 'A rule on this record'),
|
|
427
|
+
schema: t('nextcloud-vue', 'A rule on every record of this type'),
|
|
428
|
+
register: t('nextcloud-vue', 'A rule on the register'),
|
|
429
|
+
deny: t('nextcloud-vue', 'A rule that refuses'),
|
|
430
|
+
'staged-deny': t('nextcloud-vue', 'A rule that will refuse'),
|
|
431
|
+
'default-open': t('nextcloud-vue', 'No rule, so it is open'),
|
|
432
|
+
none: t('nextcloud-vue', 'No rule grants it'),
|
|
433
|
+
inherited: t('nextcloud-vue', 'A grant on a record this one hangs under'),
|
|
434
|
+
}
|
|
435
|
+
return labels[source] || source
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
</script>
|
|
440
|
+
|
|
441
|
+
<style scoped>
|
|
442
|
+
.cn-object-permissions-tab {
|
|
443
|
+
padding: 12px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.cn-object-permissions-tab__loading {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
gap: 8px;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.cn-object-permissions-tab__table {
|
|
453
|
+
width: 100%;
|
|
454
|
+
border-collapse: collapse;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.cn-object-permissions-tab__table th,
|
|
458
|
+
.cn-object-permissions-tab__table td {
|
|
459
|
+
text-align: start;
|
|
460
|
+
padding: 4px 8px;
|
|
461
|
+
border-bottom: 1px solid var(--color-border);
|
|
462
|
+
vertical-align: top;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.cn-object-permissions-tab__right,
|
|
466
|
+
.cn-object-permissions-tab__source {
|
|
467
|
+
display: block;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.cn-object-permissions-tab__hint {
|
|
471
|
+
display: block;
|
|
472
|
+
color: var(--color-text-maxcontrast);
|
|
473
|
+
font-size: 0.85em;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.cn-object-permissions-tab__unreadable {
|
|
477
|
+
color: var(--color-warning-text, var(--color-main-text));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.cn-object-permissions-tab__enforcement,
|
|
481
|
+
.cn-object-permissions-tab__empty {
|
|
482
|
+
color: var(--color-text-maxcontrast);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.cn-object-permissions-tab__history,
|
|
486
|
+
.cn-object-permissions-tab__fields {
|
|
487
|
+
margin-block-start: 24px;
|
|
488
|
+
border-top: 1px solid var(--color-border);
|
|
489
|
+
padding-block-start: 12px;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.cn-object-permissions-tab__moment {
|
|
493
|
+
display: flex;
|
|
494
|
+
flex-wrap: wrap;
|
|
495
|
+
align-items: center;
|
|
496
|
+
gap: 8px;
|
|
497
|
+
margin-block-end: 8px;
|
|
498
|
+
}
|
|
499
|
+
</style>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
3
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
4
|
+
*
|
|
5
|
+
* CnObjectPermissionsTab renders OpenRegister's answer: one row per rule, a
|
|
6
|
+
* named list of the sources it could not read, the refusal sentence on a 403,
|
|
7
|
+
* a labelled history date input, and the field-rules section only when the
|
|
8
|
+
* manifest says where the rules are declared. It is a built-in sidebar widget.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import axios from '@nextcloud/axios'
|
|
12
|
+
import { mount } from '@vue/test-utils'
|
|
13
|
+
import CnObjectPermissionsTab from '../CnObjectPermissionsTab.vue'
|
|
14
|
+
|
|
15
|
+
jest.mock('@nextcloud/axios', () => ({ __esModule: true, default: { get: jest.fn() } }))
|
|
16
|
+
jest.mock('@nextcloud/router', () => ({ __esModule: true, generateUrl: jest.fn((p) => `/index.php${p}`) }))
|
|
17
|
+
|
|
18
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Answer axios.get per URL suffix.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} table Map of URL suffix to `{status?, data}` or an Error.
|
|
24
|
+
*/
|
|
25
|
+
function answer(table) {
|
|
26
|
+
axios.get.mockImplementation((url) => {
|
|
27
|
+
const key = Object.keys(table).find((k) => url.endsWith(k))
|
|
28
|
+
const value = key ? table[key] : null
|
|
29
|
+
if (value instanceof Error) {
|
|
30
|
+
return Promise.reject(value)
|
|
31
|
+
}
|
|
32
|
+
if (value === null) {
|
|
33
|
+
const error = new Error('not found')
|
|
34
|
+
error.response = { status: 404 }
|
|
35
|
+
return Promise.reject(error)
|
|
36
|
+
}
|
|
37
|
+
return Promise.resolve({ status: value.status || 200, data: value.data })
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function mountTab(props = {}) {
|
|
42
|
+
const wrapper = mount(CnObjectPermissionsTab, {
|
|
43
|
+
propsData: { objectId: 'obj-1', register: 'reg', schema: 'sch', ...props },
|
|
44
|
+
stubs: { NcLoadingIcon: true },
|
|
45
|
+
})
|
|
46
|
+
await flush()
|
|
47
|
+
await flush()
|
|
48
|
+
await wrapper.vm.$nextTick()
|
|
49
|
+
return wrapper
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
axios.get.mockReset()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('CnObjectPermissionsTab', () => {
|
|
57
|
+
it('renders one row per rule of the object set, a deny as its own row', async () => {
|
|
58
|
+
answer({
|
|
59
|
+
'/obj-1/permissions/history': { data: { changes: [] } },
|
|
60
|
+
'/obj-1/permissions': {
|
|
61
|
+
data: {
|
|
62
|
+
denyEnforcement: 'enforcing',
|
|
63
|
+
holders: [{ rules: [{ principal: 'waarnemers', action: 'read', level: 'schema' }] }],
|
|
64
|
+
denied: [{ principal: 'waarnemers', action: 'read', level: 'object' }],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
'/api/permissions': { data: { permissions: [{ action: 'read', description: 'Open it' }] } },
|
|
68
|
+
'/api/scopes': { data: { user: 'alice', scopes: [{ schema: 'sch', provenance: {} }] } },
|
|
69
|
+
})
|
|
70
|
+
const wrapper = await mountTab()
|
|
71
|
+
const rows = wrapper.findAll('tbody tr')
|
|
72
|
+
expect(rows).toHaveLength(2)
|
|
73
|
+
expect(wrapper.text()).toContain('A rule that refuses')
|
|
74
|
+
expect(wrapper.text()).toContain('A refusal below is in force now.')
|
|
75
|
+
expect(wrapper.text()).toContain('Open it')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('names every source it could not read instead of drawing an empty list', async () => {
|
|
79
|
+
answer({
|
|
80
|
+
'/obj-1/permissions': null,
|
|
81
|
+
'/api/permissions': new Error('down'),
|
|
82
|
+
'/obj-1/shares': new Error('down'),
|
|
83
|
+
'/compare-roles': new Error('down'),
|
|
84
|
+
'/deny-preview': new Error('down'),
|
|
85
|
+
'/api/scopes': new Error('down'),
|
|
86
|
+
})
|
|
87
|
+
const wrapper = await mountTab()
|
|
88
|
+
const text = wrapper.text()
|
|
89
|
+
expect(text).toContain('This list is incomplete, not empty.')
|
|
90
|
+
expect(text).toContain('the shares on this record')
|
|
91
|
+
expect(text).toContain('the roles')
|
|
92
|
+
expect(text).toContain('the refusals')
|
|
93
|
+
expect(text).toContain('your own rights')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('says the reader may not review grants on a 403, without calling it a failed read', async () => {
|
|
97
|
+
const refusal = new Error('forbidden')
|
|
98
|
+
refusal.response = { status: 403 }
|
|
99
|
+
answer({
|
|
100
|
+
'/obj-1/permissions': refusal,
|
|
101
|
+
'/api/permissions': { data: { permissions: [] } },
|
|
102
|
+
'/obj-1/shares': { data: { results: [] } },
|
|
103
|
+
'/compare-roles': { data: { roles: [] } },
|
|
104
|
+
'/deny-preview': { data: { rules: [] } },
|
|
105
|
+
'/api/scopes': { data: { scopes: [{ schema: 'sch' }] } },
|
|
106
|
+
})
|
|
107
|
+
const wrapper = await mountTab()
|
|
108
|
+
expect(wrapper.text()).toContain('You may open this record. You may not review who else can.')
|
|
109
|
+
expect(wrapper.text()).not.toContain('incomplete, not empty')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('offers a labelled date for the history only when the object answered', async () => {
|
|
113
|
+
answer({
|
|
114
|
+
'/obj-1/permissions/history': { data: { changes: [], asOf: null } },
|
|
115
|
+
'/obj-1/permissions': { data: { holders: [] } },
|
|
116
|
+
'/api/permissions': { data: {} },
|
|
117
|
+
'/api/scopes': { data: { scopes: [{ schema: 'sch' }] } },
|
|
118
|
+
})
|
|
119
|
+
const wrapper = await mountTab()
|
|
120
|
+
const input = wrapper.find('input[type="date"]')
|
|
121
|
+
expect(input.exists()).toBe(true)
|
|
122
|
+
expect(input.element.closest('label').textContent).toContain('Report the rights as they stood on')
|
|
123
|
+
|
|
124
|
+
input.setValue('2019-03-01')
|
|
125
|
+
await input.trigger('change')
|
|
126
|
+
await flush()
|
|
127
|
+
expect(wrapper.text()).toContain('The trail does not reach 2019-03-01')
|
|
128
|
+
expect(axios.get.mock.calls.some(([url, opts]) => url.includes('/permissions/history') && opts.params.at === '2019-03-01T23:59:59')).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('names the ancestor of an inherited grant inside a sentence', async () => {
|
|
132
|
+
answer({
|
|
133
|
+
'/obj-1/permissions': null,
|
|
134
|
+
'/obj-1/shares': { data: { results: [{ principal: 'alice', actions: ['read'], inheritedFrom: 'parent-1' }] } },
|
|
135
|
+
'/api/permissions': { data: {} },
|
|
136
|
+
'/compare-roles': { data: { roles: [] } },
|
|
137
|
+
'/deny-preview': { data: { rules: [] } },
|
|
138
|
+
'/api/scopes': { data: { scopes: [{ schema: 'sch' }] } },
|
|
139
|
+
})
|
|
140
|
+
const wrapper = await mountTab()
|
|
141
|
+
expect(wrapper.text()).toContain('Granted on record parent-1, which this one hangs under')
|
|
142
|
+
expect(wrapper.text()).toContain('A grant on a record this one hangs under')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('leaves the field rules out unless the manifest says where they are declared', async () => {
|
|
146
|
+
answer({ '/obj-1/permissions': { data: { holders: [] } }, '/api/scopes': { data: { scopes: [] } }, '/api/permissions': { data: {} } })
|
|
147
|
+
const wrapper = await mountTab()
|
|
148
|
+
expect(wrapper.find('.cn-object-permissions-tab__fields').exists()).toBe(false)
|
|
149
|
+
expect(axios.get.mock.calls.some(([url]) => url.endsWith('/objects/reg/sch/obj-1'))).toBe(false)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('lists the declared field rules, marking the one OpenRegister applied to the reader', async () => {
|
|
153
|
+
answer({
|
|
154
|
+
'/objects/reg/kind/type-1': { data: { rules: [{ field: 'score', rule: 'hidden', groups: ['handlers'] }] } },
|
|
155
|
+
'/obj-1/permissions': { data: { holders: [] } },
|
|
156
|
+
'/objects/reg/sch/obj-1': { data: { '@self': { fieldRules: { hidden: ['score'] } }, kind: 'type-1' } },
|
|
157
|
+
'/api/scopes': { data: { scopes: [] } },
|
|
158
|
+
'/api/permissions': { data: {} },
|
|
159
|
+
})
|
|
160
|
+
const wrapper = await mountTab({ fieldRules: { typeField: 'kind', typeSchema: 'kind', declarationField: 'rules' } })
|
|
161
|
+
const section = wrapper.find('.cn-object-permissions-tab__fields')
|
|
162
|
+
expect(section.exists()).toBe(true)
|
|
163
|
+
expect(section.text()).toContain('Not shown')
|
|
164
|
+
expect(section.text()).toContain('applies to you')
|
|
165
|
+
expect(section.text()).toContain('handlers do not see this field.')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('lets the manifest relabel a source', async () => {
|
|
169
|
+
answer({
|
|
170
|
+
'/obj-1/permissions': { data: { holders: [{ rules: [{ principal: 'p', action: 'read', level: 'group' }] }] } },
|
|
171
|
+
'/api/scopes': { data: { scopes: [] } },
|
|
172
|
+
'/api/permissions': { data: {} },
|
|
173
|
+
})
|
|
174
|
+
const wrapper = await mountTab({ sourceLabels: { group: 'A grant on a group of case types' } })
|
|
175
|
+
expect(wrapper.text()).toContain('A grant on a group of case types')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('CnObjectSidebar built-in', () => {
|
|
180
|
+
it('resolves the object-permissions widget type', () => {
|
|
181
|
+
const fs = require('fs')
|
|
182
|
+
const path = require('path')
|
|
183
|
+
const source = fs.readFileSync(path.resolve(__dirname, '../../CnObjectSidebar/CnObjectSidebar.vue'), 'utf8')
|
|
184
|
+
expect(source).toMatch(/'object-permissions': CnObjectPermissionsTab/)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnObjectPermissionsTab — read-only "who holds which right on this object,
|
|
6
|
+
* from where, and on date X", over OpenRegister's permission reads. The
|
|
7
|
+
* companion of CnObjectAccessTab, which manages grants.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import CnObjectPermissionsTab from './CnObjectPermissionsTab.vue'
|
|
11
|
+
|
|
12
|
+
export { CnObjectPermissionsTab }
|
|
13
|
+
export default CnObjectPermissionsTab
|