qdadm 0.36.0 → 0.38.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/README.md +27 -174
- package/package.json +2 -1
- package/src/auth/SessionAuthAdapter.js +114 -3
- package/src/components/editors/PermissionEditor.vue +535 -0
- package/src/components/forms/FormField.vue +1 -11
- package/src/components/index.js +1 -0
- package/src/components/layout/AppLayout.vue +20 -8
- package/src/components/layout/defaults/DefaultToaster.vue +3 -3
- package/src/components/pages/LoginPage.vue +26 -5
- package/src/composables/useCurrentEntity.js +26 -17
- package/src/composables/useForm.js +7 -0
- package/src/composables/useFormPageBuilder.js +7 -0
- package/src/composables/useNavContext.js +30 -16
- package/src/core/index.js +0 -3
- package/src/debug/AuthCollector.js +175 -33
- package/src/debug/Collector.js +24 -2
- package/src/debug/EntitiesCollector.js +8 -0
- package/src/debug/SignalCollector.js +60 -2
- package/src/debug/components/panels/AuthPanel.vue +157 -27
- package/src/debug/components/panels/EntitiesPanel.vue +17 -1
- package/src/entity/EntityManager.js +183 -34
- package/src/entity/auth/EntityAuthAdapter.js +54 -46
- package/src/entity/auth/SecurityChecker.js +110 -42
- package/src/entity/auth/factory.js +11 -2
- package/src/entity/auth/factory.test.js +29 -0
- package/src/entity/storage/factory.test.js +6 -5
- package/src/index.js +3 -0
- package/src/kernel/Kernel.js +132 -21
- package/src/kernel/KernelContext.js +158 -0
- package/src/security/EntityRoleGranterAdapter.js +350 -0
- package/src/security/PermissionMatcher.js +148 -0
- package/src/security/PermissionRegistry.js +263 -0
- package/src/security/PersistableRoleGranterAdapter.js +618 -0
- package/src/security/RoleGranterAdapter.js +123 -0
- package/src/security/RoleGranterStorage.js +161 -0
- package/src/security/RolesManager.js +81 -0
- package/src/security/SecurityModule.js +73 -0
- package/src/security/StaticRoleGranterAdapter.js +114 -0
- package/src/security/UsersManager.js +122 -0
- package/src/security/index.js +45 -0
- package/src/security/pages/RoleForm.vue +212 -0
- package/src/security/pages/RoleList.vue +106 -0
- package/src/styles/main.css +62 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionRegistry - Central registry for all permissions
|
|
3
|
+
*
|
|
4
|
+
* Collects permissions declared by modules via ctx.permissions().
|
|
5
|
+
* Provides discovery methods for building role management UIs.
|
|
6
|
+
*
|
|
7
|
+
* Permission format: namespace:target:action
|
|
8
|
+
* - entity:books:read - Entity CRUD
|
|
9
|
+
* - auth:impersonate - System feature
|
|
10
|
+
* - admin:config:edit - Admin feature
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const registry = new PermissionRegistry()
|
|
14
|
+
*
|
|
15
|
+
* // Module registers permissions
|
|
16
|
+
* registry.register('books', { read: 'View books' }, { isEntity: true })
|
|
17
|
+
* // → entity:books:read
|
|
18
|
+
*
|
|
19
|
+
* registry.register('auth', { impersonate: 'Impersonate users' })
|
|
20
|
+
* // → auth:impersonate
|
|
21
|
+
*
|
|
22
|
+
* // Query permissions
|
|
23
|
+
* registry.getAll() // All permissions
|
|
24
|
+
* registry.getGrouped() // Grouped by namespace
|
|
25
|
+
* registry.exists('auth:impersonate') // true
|
|
26
|
+
*/
|
|
27
|
+
export class PermissionRegistry {
|
|
28
|
+
constructor() {
|
|
29
|
+
/** @type {Map<string, PermissionDefinition>} */
|
|
30
|
+
this._permissions = new Map()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register permissions for a namespace
|
|
35
|
+
*
|
|
36
|
+
* @param {string} prefix - Namespace prefix (e.g., 'books', 'auth', 'admin:config')
|
|
37
|
+
* @param {Object<string, string|PermissionMeta>} permissions - Permission definitions
|
|
38
|
+
* @param {Object} [options]
|
|
39
|
+
* @param {boolean} [options.isEntity=false] - Prefix with 'entity:' namespace
|
|
40
|
+
* @param {string} [options.module] - Module name for tracking
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Entity permissions (auto-prefixed with 'entity:')
|
|
44
|
+
* registry.register('books', {
|
|
45
|
+
* read: 'View books',
|
|
46
|
+
* checkout: { label: 'Checkout', description: 'Borrow books' }
|
|
47
|
+
* }, { isEntity: true })
|
|
48
|
+
* // → entity:books:read, entity:books:checkout
|
|
49
|
+
*
|
|
50
|
+
* // System permissions
|
|
51
|
+
* registry.register('auth', {
|
|
52
|
+
* impersonate: 'Impersonate users'
|
|
53
|
+
* })
|
|
54
|
+
* // → auth:impersonate
|
|
55
|
+
*/
|
|
56
|
+
register(prefix, permissions, options = {}) {
|
|
57
|
+
const namespace = options.isEntity ? `entity:${prefix}` : prefix
|
|
58
|
+
const module = options.module || null
|
|
59
|
+
|
|
60
|
+
for (const [action, meta] of Object.entries(permissions)) {
|
|
61
|
+
const key = `${namespace}:${action}`
|
|
62
|
+
const definition = typeof meta === 'string'
|
|
63
|
+
? { label: meta }
|
|
64
|
+
: { ...meta }
|
|
65
|
+
|
|
66
|
+
this._permissions.set(key, {
|
|
67
|
+
key,
|
|
68
|
+
namespace,
|
|
69
|
+
action,
|
|
70
|
+
module,
|
|
71
|
+
label: definition.label || action,
|
|
72
|
+
description: definition.description || null,
|
|
73
|
+
custom: definition.custom || false
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register standard CRUD permissions for an entity
|
|
80
|
+
* Called automatically by ctx.entity()
|
|
81
|
+
*
|
|
82
|
+
* @param {string} entityName - Entity name
|
|
83
|
+
* @param {Object} [options]
|
|
84
|
+
* @param {string} [options.module] - Module name
|
|
85
|
+
* @param {string[]} [options.actions] - Custom action list (default: CRUD)
|
|
86
|
+
* @param {boolean} [options.hasOwnership] - Register entity-own:* permissions too
|
|
87
|
+
* @param {string[]} [options.ownActions] - Actions for ownership (default: same as actions)
|
|
88
|
+
*/
|
|
89
|
+
registerEntity(entityName, options = {}) {
|
|
90
|
+
const actions = options.actions || ['read', 'list', 'create', 'update', 'delete']
|
|
91
|
+
const permissions = {}
|
|
92
|
+
|
|
93
|
+
for (const action of actions) {
|
|
94
|
+
permissions[action] = {
|
|
95
|
+
label: `${this._capitalize(action)} ${entityName}`,
|
|
96
|
+
description: `Can ${action} ${entityName} records`
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.register(entityName, permissions, {
|
|
101
|
+
isEntity: true,
|
|
102
|
+
module: options.module
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Register entity-own:* permissions for ownership-based access
|
|
106
|
+
if (options.hasOwnership) {
|
|
107
|
+
const ownActions = options.ownActions || actions.filter(a => a !== 'list' && a !== 'create')
|
|
108
|
+
const ownPermissions = {}
|
|
109
|
+
|
|
110
|
+
for (const action of ownActions) {
|
|
111
|
+
ownPermissions[action] = {
|
|
112
|
+
label: `${this._capitalize(action)} own ${entityName}`,
|
|
113
|
+
description: `Can ${action} own ${entityName} records`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Register under entity-own:entityName namespace
|
|
118
|
+
this.register(`entity-own:${entityName}`, ownPermissions, {
|
|
119
|
+
module: options.module
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Unregister all permissions for a namespace
|
|
126
|
+
* @param {string} namespace - Namespace to clear
|
|
127
|
+
*/
|
|
128
|
+
unregister(namespace) {
|
|
129
|
+
for (const key of this._permissions.keys()) {
|
|
130
|
+
if (key.startsWith(namespace + ':')) {
|
|
131
|
+
this._permissions.delete(key)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get all registered permissions
|
|
138
|
+
* @returns {PermissionDefinition[]}
|
|
139
|
+
*/
|
|
140
|
+
getAll() {
|
|
141
|
+
return [...this._permissions.values()]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all permission keys
|
|
146
|
+
* @returns {string[]}
|
|
147
|
+
*/
|
|
148
|
+
getKeys() {
|
|
149
|
+
return [...this._permissions.keys()]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get permissions grouped by namespace
|
|
154
|
+
* @returns {Object<string, PermissionDefinition[]>}
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* registry.getGrouped()
|
|
158
|
+
* // {
|
|
159
|
+
* // 'entity:books': [{ key: 'entity:books:read', ... }],
|
|
160
|
+
* // 'auth': [{ key: 'auth:impersonate', ... }]
|
|
161
|
+
* // }
|
|
162
|
+
*/
|
|
163
|
+
getGrouped() {
|
|
164
|
+
const groups = {}
|
|
165
|
+
|
|
166
|
+
for (const perm of this._permissions.values()) {
|
|
167
|
+
if (!groups[perm.namespace]) {
|
|
168
|
+
groups[perm.namespace] = []
|
|
169
|
+
}
|
|
170
|
+
groups[perm.namespace].push(perm)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return groups
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get permissions for a specific namespace
|
|
178
|
+
* @param {string} namespace - Namespace prefix
|
|
179
|
+
* @returns {PermissionDefinition[]}
|
|
180
|
+
*/
|
|
181
|
+
getByNamespace(namespace) {
|
|
182
|
+
return this.getAll().filter(p =>
|
|
183
|
+
p.namespace === namespace || p.namespace.startsWith(namespace + ':')
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get permissions registered by a specific module
|
|
189
|
+
* @param {string} moduleName - Module name
|
|
190
|
+
* @returns {PermissionDefinition[]}
|
|
191
|
+
*/
|
|
192
|
+
getByModule(moduleName) {
|
|
193
|
+
return this.getAll().filter(p => p.module === moduleName)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get entity permissions only
|
|
198
|
+
* @returns {PermissionDefinition[]}
|
|
199
|
+
*/
|
|
200
|
+
getEntityPermissions() {
|
|
201
|
+
return this.getAll().filter(p => p.namespace.startsWith('entity:'))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get non-entity permissions (system, feature, admin, etc.)
|
|
206
|
+
* @returns {PermissionDefinition[]}
|
|
207
|
+
*/
|
|
208
|
+
getSystemPermissions() {
|
|
209
|
+
return this.getAll().filter(p => !p.namespace.startsWith('entity:'))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a permission is registered
|
|
214
|
+
* @param {string} permission - Permission key
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
exists(permission) {
|
|
218
|
+
return this._permissions.has(permission)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get a specific permission definition
|
|
223
|
+
* @param {string} permission - Permission key
|
|
224
|
+
* @returns {PermissionDefinition|null}
|
|
225
|
+
*/
|
|
226
|
+
get(permission) {
|
|
227
|
+
return this._permissions.get(permission) || null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get count of registered permissions
|
|
232
|
+
* @returns {number}
|
|
233
|
+
*/
|
|
234
|
+
get size() {
|
|
235
|
+
return this._permissions.size
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Capitalize first letter
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
_capitalize(str) {
|
|
243
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @typedef {Object} PermissionDefinition
|
|
249
|
+
* @property {string} key - Full permission key (e.g., 'entity:books:read')
|
|
250
|
+
* @property {string} namespace - Namespace (e.g., 'entity:books')
|
|
251
|
+
* @property {string} action - Action name (e.g., 'read')
|
|
252
|
+
* @property {string|null} module - Module that registered this permission
|
|
253
|
+
* @property {string} label - Human-readable label
|
|
254
|
+
* @property {string|null} description - Optional description
|
|
255
|
+
* @property {boolean} custom - Is this a custom (non-CRUD) permission
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @typedef {Object} PermissionMeta
|
|
260
|
+
* @property {string} [label] - Human-readable label
|
|
261
|
+
* @property {string} [description] - Description
|
|
262
|
+
* @property {boolean} [custom] - Mark as custom permission
|
|
263
|
+
*/
|