@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,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Who holds which right on one OpenRegister object, and where each grant came
|
|
6
|
+
* from. The reads behind CnObjectPermissionsTab.
|
|
7
|
+
*
|
|
8
|
+
* Every function here is a read. Nothing decides who may open an object: the
|
|
9
|
+
* grant, its provenance, the deny and the inheritance live in OpenRegister, and
|
|
10
|
+
* these helpers hand on what it answered. A deny is never subtracted from a
|
|
11
|
+
* grant, a missing answer is never filled with a default, and nothing is
|
|
12
|
+
* cached: a copy of an access decision is a second decision nobody updates.
|
|
13
|
+
*
|
|
14
|
+
* The object's own permission set (`/permissions`) is preferred; an older
|
|
15
|
+
* OpenRegister answers 404 to it, and then five reads are the fallback:
|
|
16
|
+
*
|
|
17
|
+
* GET {apiBase}/permissions the verb catalogue + deny mode
|
|
18
|
+
* GET {apiBase}/objects/{register}/{schema}/{id}/shares grants written on the object
|
|
19
|
+
* GET {apiBase}/permissions/compare-roles?register= each role and its verbs
|
|
20
|
+
* GET {apiBase}/scopes?register=&schema= the caller's own verdict + provenance
|
|
21
|
+
* GET {apiBase}/permissions/deny-preview?register=&schema= the deny rules as written
|
|
22
|
+
*
|
|
23
|
+
* plus the history read `{...}/{id}/permissions/history?at=` for "who held a
|
|
24
|
+
* right on a date".
|
|
25
|
+
*
|
|
26
|
+
* A failed read answers `null`, never `[]`: "we could not ask" and "nobody
|
|
27
|
+
* holds this" are opposite answers that would otherwise render as the same
|
|
28
|
+
* empty table.
|
|
29
|
+
*/
|
|
30
|
+
import axios from '@nextcloud/axios'
|
|
31
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
32
|
+
import { prefixUrl } from './headers.js'
|
|
33
|
+
|
|
34
|
+
/** The OpenRegister API root a target defaults to. */
|
|
35
|
+
export const DEFAULT_API_BASE = '/apps/openregister/api'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalise a target to `{register, schema, apiBase}`.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} target The object's register, schema and optional apiBase.
|
|
41
|
+
* @return {{register: string, schema: string, apiBase: string}} The target.
|
|
42
|
+
*/
|
|
43
|
+
function normalise(target = {}) {
|
|
44
|
+
return {
|
|
45
|
+
register: String(target.register || ''),
|
|
46
|
+
schema: String(target.schema || ''),
|
|
47
|
+
apiBase: String(target.apiBase || DEFAULT_API_BASE).replace(/\/$/, ''),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GET one path, keeping the status beside the body.
|
|
53
|
+
*
|
|
54
|
+
* The status is part of the answer: a 403 on the access set means the reader
|
|
55
|
+
* may open the object and may not enumerate who else can, which is not a
|
|
56
|
+
* failed read. A transport failure has no status and reports 0. A non-JSON
|
|
57
|
+
* body (the SPA shell answering 200) is a failed read, not "no rules".
|
|
58
|
+
*
|
|
59
|
+
* @param {string} url App-absolute URL.
|
|
60
|
+
* @param {object} params Query parameters.
|
|
61
|
+
* @return {Promise<{status: number, data: object|null}>} The status and the body.
|
|
62
|
+
*/
|
|
63
|
+
async function readResult(url, params = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const response = await axios.get(prefixUrl(url), { params })
|
|
66
|
+
if (typeof response?.data !== 'object' || response.data === null) {
|
|
67
|
+
return { status: response?.status || 0, data: null }
|
|
68
|
+
}
|
|
69
|
+
return { status: response.status || 200, data: response.data }
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { status: error?.response?.status || 0, data: null }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GET one path, answering null when it cannot be read.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} url App-absolute URL.
|
|
79
|
+
* @param {object} params Query parameters.
|
|
80
|
+
* @return {Promise<object|null>} The body, or null.
|
|
81
|
+
*/
|
|
82
|
+
async function read(url, params = {}) {
|
|
83
|
+
const { data } = await readResult(url, params)
|
|
84
|
+
return data
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The URL of one object, under its target.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} target The target.
|
|
91
|
+
* @param {string} objectId The object id.
|
|
92
|
+
* @param {string} [schema] A schema other than the target's.
|
|
93
|
+
* @return {string} The URL.
|
|
94
|
+
*/
|
|
95
|
+
function objectUrl(target, objectId, schema) {
|
|
96
|
+
const { register, apiBase } = normalise(target)
|
|
97
|
+
return `${apiBase}/objects/${register}/${schema || normalise(target).schema}/${objectId}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The grantable verbs and the deny enforcement mode.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} target The target (only `apiBase` is read).
|
|
104
|
+
* @return {Promise<object|null>} `{permissions, denyEnforcement}`, or null.
|
|
105
|
+
*/
|
|
106
|
+
export function fetchPermissionCatalogue(target) {
|
|
107
|
+
return read(`${normalise(target).apiBase}/permissions`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The grants written on one object.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} target The target.
|
|
114
|
+
* @param {string} objectId The object id.
|
|
115
|
+
* @return {Promise<Array|null>} The grants, or null.
|
|
116
|
+
*/
|
|
117
|
+
export async function fetchObjectGrants(target, objectId) {
|
|
118
|
+
const body = await read(`${objectUrl(target, objectId)}/shares`)
|
|
119
|
+
if (body === null || !Array.isArray(body.results)) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
return body.results
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Each role in the register and the verbs it holds.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} target The target.
|
|
129
|
+
* @return {Promise<object|null>} `{roles}`, or null.
|
|
130
|
+
*/
|
|
131
|
+
export function fetchRoleGrants(target) {
|
|
132
|
+
const { apiBase, register } = normalise(target)
|
|
133
|
+
return read(`${apiBase}/permissions/compare-roles`, { register })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The caller's own effective verbs on the schema, with their provenance.
|
|
138
|
+
*
|
|
139
|
+
* The endpoint answers a matrix even when narrowed, so the row is picked by
|
|
140
|
+
* schema name rather than by position.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} target The target.
|
|
143
|
+
* @return {Promise<object|null>} The scope entry, or null.
|
|
144
|
+
*/
|
|
145
|
+
export async function fetchCallerScope(target) {
|
|
146
|
+
const { apiBase, register, schema } = normalise(target)
|
|
147
|
+
const body = await read(`${apiBase}/scopes`, { register, schema })
|
|
148
|
+
if (body === null || !Array.isArray(body.scopes)) {
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
const scope = body.scopes.find((entry) => entry?.schema === schema)
|
|
152
|
+
if (!scope) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
return { user: body.user, isAdmin: body.isAdmin, groups: body.groups, ...scope }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The deny rules as written for the register and schema.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} target The target.
|
|
162
|
+
* @return {Promise<object|null>} `{denyEnforcement, enforcing, rules}`, or null.
|
|
163
|
+
*/
|
|
164
|
+
export function fetchDenyRules(target) {
|
|
165
|
+
const { apiBase, register, schema } = normalise(target)
|
|
166
|
+
return read(`${apiBase}/permissions/deny-preview`, { register, schema })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The permission set OpenRegister publishes for this object, with its status.
|
|
171
|
+
*
|
|
172
|
+
* @param {object} target The target.
|
|
173
|
+
* @param {string} objectId The object id.
|
|
174
|
+
* @return {Promise<{status: number, set: object|null}>} The status and the set.
|
|
175
|
+
*/
|
|
176
|
+
export async function fetchObjectPermissions(target, objectId) {
|
|
177
|
+
const { status, data } = await readResult(`${objectUrl(target, objectId)}/permissions`)
|
|
178
|
+
if (data === null || Array.isArray(data.holders) === false) {
|
|
179
|
+
return { status, set: null }
|
|
180
|
+
}
|
|
181
|
+
return { status, set: data }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The set as it stood at a moment, from the object's audit trail.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} target The target.
|
|
188
|
+
* @param {string} objectId The object id.
|
|
189
|
+
* @param {string} at An ISO-8601 moment.
|
|
190
|
+
* @return {Promise<{status: number, history: object|null}>} The status and the history.
|
|
191
|
+
*/
|
|
192
|
+
export async function fetchAccessHistory(target, objectId, at) {
|
|
193
|
+
const { status, data } = await readResult(`${objectUrl(target, objectId)}/permissions/history`, { at })
|
|
194
|
+
if (data === null || Array.isArray(data.changes) === false) {
|
|
195
|
+
return { status, history: null }
|
|
196
|
+
}
|
|
197
|
+
return { status, history: data }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The end and the area written on one rule, as OpenRegister reported them.
|
|
202
|
+
*
|
|
203
|
+
* Neither is compared to anything: whether an expired grant still answers is
|
|
204
|
+
* resolved in OpenRegister. A role grant carries the whole holder list, so the
|
|
205
|
+
* entry naming this principal is picked out of it.
|
|
206
|
+
*
|
|
207
|
+
* @param {object|Array|string|null} rule The rule as written.
|
|
208
|
+
* @param {string} principal The holder of the row.
|
|
209
|
+
* @return {{until: string, scopedTo: object|null}} The end and the area.
|
|
210
|
+
*/
|
|
211
|
+
export function constraintsOf(rule, principal) {
|
|
212
|
+
const none = { until: '', scopedTo: null }
|
|
213
|
+
if (rule === null || typeof rule !== 'object') {
|
|
214
|
+
return none
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let entry = rule
|
|
218
|
+
if (Array.isArray(rule)) {
|
|
219
|
+
entry = rule.find((candidate) => candidate === principal
|
|
220
|
+
|| (candidate !== null
|
|
221
|
+
&& typeof candidate === 'object'
|
|
222
|
+
&& [candidate.principal, candidate.group, candidate.role, candidate.user].includes(principal)))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
226
|
+
return none
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
until: typeof entry.until === 'string' ? entry.until : '',
|
|
231
|
+
scopedTo: entry.scopedTo || null,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One reported rule as a row.
|
|
237
|
+
*
|
|
238
|
+
* @param {object} rule The rule.
|
|
239
|
+
* @param {boolean} refusing Whether the rule takes a right away.
|
|
240
|
+
* @param {boolean} enforcing Whether refusals are in force.
|
|
241
|
+
* @return {object} The row.
|
|
242
|
+
*/
|
|
243
|
+
function ruleRow(rule, refusing, enforcing) {
|
|
244
|
+
const principal = rule?.principal || ''
|
|
245
|
+
const { until, scopedTo } = constraintsOf(rule?.rule ?? null, principal)
|
|
246
|
+
|
|
247
|
+
let source = rule?.level || 'object'
|
|
248
|
+
if (refusing === true) {
|
|
249
|
+
source = enforcing === true ? 'deny' : 'staged-deny'
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
holder: principal,
|
|
254
|
+
right: rule?.action || '',
|
|
255
|
+
source,
|
|
256
|
+
detail: rule?.role || '',
|
|
257
|
+
inheritedFrom: rule?.inheritedFrom || '',
|
|
258
|
+
level: rule?.level || '',
|
|
259
|
+
role: rule?.role || '',
|
|
260
|
+
declared: rule?.declared !== false,
|
|
261
|
+
conditional: rule?.conditional === true,
|
|
262
|
+
until,
|
|
263
|
+
scopedTo,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The object's own permission set as rows. A deny is its own row.
|
|
269
|
+
*
|
|
270
|
+
* @param {object|null} set The set from `fetchObjectPermissions`.
|
|
271
|
+
* @return {Array<object>} The rows.
|
|
272
|
+
*/
|
|
273
|
+
export function objectPermissionRows(set) {
|
|
274
|
+
if (set === null || typeof set !== 'object') {
|
|
275
|
+
return []
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const enforcing = set.denyEnforcement === 'enforcing'
|
|
279
|
+
const rows = []
|
|
280
|
+
for (const holder of set.holders || []) {
|
|
281
|
+
for (const rule of holder?.rules || []) {
|
|
282
|
+
rows.push(ruleRow(rule, false, enforcing))
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const rule of set.denied || []) {
|
|
286
|
+
rows.push(ruleRow(rule, true, enforcing))
|
|
287
|
+
}
|
|
288
|
+
return rows
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The set as it stood at the moment asked about.
|
|
293
|
+
*
|
|
294
|
+
* `asOf: null` (the trail does not reach back that far) is "unanswered", never
|
|
295
|
+
* an empty table: an empty table would claim nobody held a right that day.
|
|
296
|
+
*
|
|
297
|
+
* @param {object|null} history The history from `fetchAccessHistory`.
|
|
298
|
+
* @return {{answered: boolean, at: string, rows: Array<object>, setBy: string, changedAfterwardsBy: object|null}} The answer.
|
|
299
|
+
*/
|
|
300
|
+
export function asOfRows(history) {
|
|
301
|
+
const unanswered = { answered: false, at: history?.at || '', rows: [], setBy: '', changedAfterwardsBy: null }
|
|
302
|
+
|
|
303
|
+
const asOf = history?.asOf || null
|
|
304
|
+
if (asOf === null || typeof asOf !== 'object') {
|
|
305
|
+
return unanswered
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const rows = []
|
|
309
|
+
for (const holder of asOf.holders || []) {
|
|
310
|
+
for (const rule of holder?.rules || []) {
|
|
311
|
+
rows.push(ruleRow(rule, false, false))
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
answered: true,
|
|
317
|
+
at: asOf.at || history?.at || '',
|
|
318
|
+
rows,
|
|
319
|
+
setBy: asOf.setBy || '',
|
|
320
|
+
changedAfterwardsBy: asOf.changedAfterwardsBy || null,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* All answers as one list of rows. A renderer, not an evaluator: each row
|
|
326
|
+
* restates ONE reported rule, and no row is the product of two.
|
|
327
|
+
*
|
|
328
|
+
* When the object answered for itself the share, role and deny reads are not
|
|
329
|
+
* replayed on top (the same grant would be counted twice). The caller's own
|
|
330
|
+
* verdict always rides along last, because it is the one answer about this
|
|
331
|
+
* reader rather than a rule about everybody.
|
|
332
|
+
*
|
|
333
|
+
* @param {object} answers The reads.
|
|
334
|
+
* @param {object|null} answers.objectPermissions The object's own set.
|
|
335
|
+
* @param {Array|null} answers.objectGrants Grants on the object.
|
|
336
|
+
* @param {object|null} answers.roleGrants Roles and their verbs.
|
|
337
|
+
* @param {object|null} answers.callerScope The caller's own verdict.
|
|
338
|
+
* @param {object|null} answers.denyRules The deny rules.
|
|
339
|
+
* @return {Array<object>} The rows.
|
|
340
|
+
*/
|
|
341
|
+
export function grantRows({ objectPermissions = null, objectGrants, roleGrants, callerScope, denyRules }) {
|
|
342
|
+
const rows = []
|
|
343
|
+
|
|
344
|
+
if (objectPermissions !== null) {
|
|
345
|
+
rows.push(...objectPermissionRows(objectPermissions))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const grant of (objectPermissions === null ? objectGrants : null) || []) {
|
|
349
|
+
for (const right of grant?.actions || []) {
|
|
350
|
+
rows.push({
|
|
351
|
+
holder: grant.principal || grant.userId || grant.groupId || '',
|
|
352
|
+
right,
|
|
353
|
+
// `inheritedFrom` is the id of the ancestor, not a source key.
|
|
354
|
+
source: grant.source || (grant.inheritedFrom ? 'inherited' : 'share'),
|
|
355
|
+
inheritedFrom: grant.inheritedFrom || '',
|
|
356
|
+
detail: grant.expires ? String(grant.expires) : '',
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const role of (objectPermissions === null ? roleGrants?.roles : null) || []) {
|
|
362
|
+
for (const right of role?.actions || []) {
|
|
363
|
+
rows.push({ holder: role.role || '', right, source: 'role', detail: '' })
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const rule of (objectPermissions === null ? denyRules?.rules : null) || []) {
|
|
368
|
+
rows.push({
|
|
369
|
+
holder: rule.principal || '',
|
|
370
|
+
right: rule.action || '',
|
|
371
|
+
source: denyRules.enforcing ? 'deny' : 'staged-deny',
|
|
372
|
+
detail: rule.level ? `${rule.level}: ${rule.subject || ''}` : '',
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const [right, record] of Object.entries(callerScope?.provenance || {})) {
|
|
377
|
+
rows.push({
|
|
378
|
+
holder: callerScope.user || '',
|
|
379
|
+
right,
|
|
380
|
+
source: record?.source || 'unknown',
|
|
381
|
+
granted: record?.granted,
|
|
382
|
+
detail: record?.role || record?.principal || '',
|
|
383
|
+
stagedDeny: record?.stagedDeny || null,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return rows
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The id a reference carries, as a uuid string or as an expanded object.
|
|
392
|
+
*
|
|
393
|
+
* @param {unknown} value The reference.
|
|
394
|
+
* @return {string} The id, or ''.
|
|
395
|
+
*/
|
|
396
|
+
function referenceId(value) {
|
|
397
|
+
if (value && typeof value === 'object') {
|
|
398
|
+
return String(value['@self']?.id ?? value.id ?? value.uuid ?? '')
|
|
399
|
+
}
|
|
400
|
+
return String(value ?? '')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The object's decided field rules, and the per-role field rules its TYPE
|
|
405
|
+
* object declares.
|
|
406
|
+
*
|
|
407
|
+
* Two reads, because they answer different questions: `@self.fieldRules` is
|
|
408
|
+
* what OpenRegister decided for THIS reader, the declaration on the type
|
|
409
|
+
* object is the rule behind it. Neither is evaluated here.
|
|
410
|
+
*
|
|
411
|
+
* @param {object} target The target.
|
|
412
|
+
* @param {string} objectId The object id.
|
|
413
|
+
* @param {object} config Where the declaration lives.
|
|
414
|
+
* @param {string} config.typeField Field on the object referencing its type object.
|
|
415
|
+
* @param {string} config.typeSchema Schema of the type object (same register).
|
|
416
|
+
* @param {string} [config.declarationField] Field on the type object holding the rules.
|
|
417
|
+
* @return {Promise<{decided: object|null, declared: Array<object>|null}>} The answer.
|
|
418
|
+
*/
|
|
419
|
+
export async function fetchFieldRoleRules(target, objectId, config) {
|
|
420
|
+
if (!objectId || !config?.typeField || !config?.typeSchema) {
|
|
421
|
+
return { decided: null, declared: null }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const object = await read(objectUrl(target, objectId))
|
|
425
|
+
if (object === null) {
|
|
426
|
+
return { decided: null, declared: null }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const decided = object['@self']?.fieldRules ?? null
|
|
430
|
+
const typeId = referenceId(object[config.typeField])
|
|
431
|
+
if (!typeId) {
|
|
432
|
+
return { decided, declared: null }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const type = await read(objectUrl(target, typeId, config.typeSchema))
|
|
436
|
+
if (type === null) {
|
|
437
|
+
return { decided, declared: null }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const declared = type[config.declarationField || 'fieldRoleRules']
|
|
441
|
+
return { decided, declared: Array.isArray(declared) ? declared : [] }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The two things a role rule can do to a field. The spelling is OpenRegister's
|
|
446
|
+
* contract (`hidden`, `readOnly`); any other rule is dropped.
|
|
447
|
+
*/
|
|
448
|
+
export const ROLE_RULES = ['hidden', 'readOnly']
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The reader's word for each field rule.
|
|
452
|
+
*
|
|
453
|
+
* @return {object} The labels, by rule id.
|
|
454
|
+
*/
|
|
455
|
+
export function roleRuleLabels() {
|
|
456
|
+
return {
|
|
457
|
+
hidden: t('nextcloud-vue', 'Not shown'),
|
|
458
|
+
readOnly: t('nextcloud-vue', 'Cannot be changed'),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* A declared list of names, blanks and repeats removed.
|
|
464
|
+
*
|
|
465
|
+
* @param {unknown} value The list.
|
|
466
|
+
* @return {Array<string>} The names.
|
|
467
|
+
*/
|
|
468
|
+
function names(value) {
|
|
469
|
+
if (Array.isArray(value) === false) {
|
|
470
|
+
return []
|
|
471
|
+
}
|
|
472
|
+
const found = []
|
|
473
|
+
value.forEach((entry) => {
|
|
474
|
+
const name = String(entry ?? '').trim()
|
|
475
|
+
if (name !== '' && found.includes(name) === false) {
|
|
476
|
+
found.push(name)
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
return found
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Whether OpenRegister applied this rule to the reader. False when the object
|
|
484
|
+
* carries no decision at all (an older OpenRegister), never invented.
|
|
485
|
+
*
|
|
486
|
+
* @param {string} field The field.
|
|
487
|
+
* @param {string} rule The rule.
|
|
488
|
+
* @param {object|null} decided The object's `@self.fieldRules`.
|
|
489
|
+
* @return {boolean} True when the reader met this rule.
|
|
490
|
+
*/
|
|
491
|
+
function appliesToMe(field, rule, decided) {
|
|
492
|
+
const applied = decided?.[rule]
|
|
493
|
+
if (Array.isArray(applied) === false) {
|
|
494
|
+
return false
|
|
495
|
+
}
|
|
496
|
+
return applied.map((name) => String(name)).includes(field)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The declared field rules as rows. A row naming no field, or an unknown rule,
|
|
501
|
+
* is dropped rather than rendered blank.
|
|
502
|
+
*
|
|
503
|
+
* @param {Array<object>|null} declared The declaration.
|
|
504
|
+
* @param {object|null} decided The object's `@self.fieldRules`.
|
|
505
|
+
* @return {Array<object>} The rows.
|
|
506
|
+
*/
|
|
507
|
+
export function fieldRoleRows(declared, decided) {
|
|
508
|
+
if (Array.isArray(declared) === false) {
|
|
509
|
+
return []
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return declared
|
|
513
|
+
.filter((rule) => rule && ROLE_RULES.includes(rule.rule) && typeof rule.field === 'string' && rule.field.trim() !== '')
|
|
514
|
+
.map((rule) => {
|
|
515
|
+
const field = rule.field.trim()
|
|
516
|
+
return {
|
|
517
|
+
field,
|
|
518
|
+
rule: rule.rule,
|
|
519
|
+
groups: names(rule.groups),
|
|
520
|
+
heldBy: names(rule.heldBy),
|
|
521
|
+
reason: String(rule.reason ?? '').trim(),
|
|
522
|
+
appliesToMe: appliesToMe(field, rule.rule, decided),
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* One field-rule row as a whole sentence (whole sentences, so a translator can
|
|
529
|
+
* reorder the groups).
|
|
530
|
+
*
|
|
531
|
+
* @param {object} row One row from `fieldRoleRows`.
|
|
532
|
+
* @return {string} The sentence.
|
|
533
|
+
*/
|
|
534
|
+
export function roleRuleSentence(row) {
|
|
535
|
+
const groups = row?.groups?.join(', ') ?? ''
|
|
536
|
+
const heldBy = row?.heldBy?.join(', ') ?? ''
|
|
537
|
+
|
|
538
|
+
if (row?.rule === 'hidden' && heldBy !== '') {
|
|
539
|
+
return t('nextcloud-vue', '{groups} do not see this field. {heldBy} do.', { groups, heldBy })
|
|
540
|
+
}
|
|
541
|
+
if (row?.rule === 'hidden') {
|
|
542
|
+
return t('nextcloud-vue', '{groups} do not see this field.', { groups })
|
|
543
|
+
}
|
|
544
|
+
if (heldBy !== '') {
|
|
545
|
+
return t('nextcloud-vue', '{groups} cannot change this field. {heldBy} can.', { groups, heldBy })
|
|
546
|
+
}
|
|
547
|
+
return t('nextcloud-vue', '{groups} cannot change this field.', { groups })
|
|
548
|
+
}
|