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.
Files changed (43) hide show
  1. package/README.md +27 -174
  2. package/package.json +2 -1
  3. package/src/auth/SessionAuthAdapter.js +114 -3
  4. package/src/components/editors/PermissionEditor.vue +535 -0
  5. package/src/components/forms/FormField.vue +1 -11
  6. package/src/components/index.js +1 -0
  7. package/src/components/layout/AppLayout.vue +20 -8
  8. package/src/components/layout/defaults/DefaultToaster.vue +3 -3
  9. package/src/components/pages/LoginPage.vue +26 -5
  10. package/src/composables/useCurrentEntity.js +26 -17
  11. package/src/composables/useForm.js +7 -0
  12. package/src/composables/useFormPageBuilder.js +7 -0
  13. package/src/composables/useNavContext.js +30 -16
  14. package/src/core/index.js +0 -3
  15. package/src/debug/AuthCollector.js +175 -33
  16. package/src/debug/Collector.js +24 -2
  17. package/src/debug/EntitiesCollector.js +8 -0
  18. package/src/debug/SignalCollector.js +60 -2
  19. package/src/debug/components/panels/AuthPanel.vue +157 -27
  20. package/src/debug/components/panels/EntitiesPanel.vue +17 -1
  21. package/src/entity/EntityManager.js +183 -34
  22. package/src/entity/auth/EntityAuthAdapter.js +54 -46
  23. package/src/entity/auth/SecurityChecker.js +110 -42
  24. package/src/entity/auth/factory.js +11 -2
  25. package/src/entity/auth/factory.test.js +29 -0
  26. package/src/entity/storage/factory.test.js +6 -5
  27. package/src/index.js +3 -0
  28. package/src/kernel/Kernel.js +132 -21
  29. package/src/kernel/KernelContext.js +158 -0
  30. package/src/security/EntityRoleGranterAdapter.js +350 -0
  31. package/src/security/PermissionMatcher.js +148 -0
  32. package/src/security/PermissionRegistry.js +263 -0
  33. package/src/security/PersistableRoleGranterAdapter.js +618 -0
  34. package/src/security/RoleGranterAdapter.js +123 -0
  35. package/src/security/RoleGranterStorage.js +161 -0
  36. package/src/security/RolesManager.js +81 -0
  37. package/src/security/SecurityModule.js +73 -0
  38. package/src/security/StaticRoleGranterAdapter.js +114 -0
  39. package/src/security/UsersManager.js +122 -0
  40. package/src/security/index.js +45 -0
  41. package/src/security/pages/RoleForm.vue +212 -0
  42. package/src/security/pages/RoleList.vue +106 -0
  43. 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
+ */