qdadm 0.36.0 → 0.38.1
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
package/src/kernel/Kernel.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
import { createApp, h } from 'vue'
|
|
40
|
+
import { createApp, h, defineComponent } from 'vue'
|
|
41
41
|
import { createPinia } from 'pinia'
|
|
42
42
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
43
43
|
import ToastService from 'primevue/toastservice'
|
|
@@ -55,6 +55,8 @@ import { registerStandardZones } from '../zones/zones.js'
|
|
|
55
55
|
import { createHookRegistry } from '../hooks/HookRegistry.js'
|
|
56
56
|
import { createSecurityChecker } from '../entity/auth/SecurityChecker.js'
|
|
57
57
|
import { authFactory, CompositeAuthAdapter } from '../entity/auth/index.js'
|
|
58
|
+
import { PermissionRegistry } from '../security/PermissionRegistry.js'
|
|
59
|
+
import { StaticRoleGranterAdapter } from '../security/StaticRoleGranterAdapter.js'
|
|
58
60
|
import { createManagers } from '../entity/factory.js'
|
|
59
61
|
import { defaultStorageResolver } from '../entity/storage/factory.js'
|
|
60
62
|
import { createDeferredRegistry } from '../deferred/DeferredRegistry.js'
|
|
@@ -131,6 +133,8 @@ export class Kernel {
|
|
|
131
133
|
this.sseBridge = null
|
|
132
134
|
this.layoutComponents = null
|
|
133
135
|
this.securityChecker = null
|
|
136
|
+
/** @type {import('../security/PermissionRegistry.js').PermissionRegistry|null} */
|
|
137
|
+
this.permissionRegistry = null
|
|
134
138
|
/** @type {import('./ModuleLoader.js').ModuleLoader|null} */
|
|
135
139
|
this.moduleLoader = null
|
|
136
140
|
/** @type {Map<string|symbol, any>} Pending provides from modules (applied after vueApp creation) */
|
|
@@ -185,6 +189,10 @@ export class Kernel {
|
|
|
185
189
|
this._createDeferredRegistry()
|
|
186
190
|
// 2. Create orchestrator early (modules need it for ctx.entity())
|
|
187
191
|
this._createOrchestrator()
|
|
192
|
+
// 2.5. Create PermissionRegistry early (modules register permissions via ctx.entity())
|
|
193
|
+
this._createPermissionRegistry()
|
|
194
|
+
// 2.6. Setup security early (SecurityModule needs ctx.security)
|
|
195
|
+
this._setupSecurity()
|
|
188
196
|
// 3. Register auth:ready deferred (if auth configured)
|
|
189
197
|
this._registerAuthDeferred()
|
|
190
198
|
// 4. Initialize legacy modules (can use all services, registers routes)
|
|
@@ -197,13 +205,14 @@ export class Kernel {
|
|
|
197
205
|
this._setupAuthGuard()
|
|
198
206
|
// 6. Setup auth:expired handler (needs router + authAdapter)
|
|
199
207
|
this._setupAuthExpiredHandler()
|
|
208
|
+
// 6.5. Setup auth impersonation (authAdapter reacts to signals)
|
|
209
|
+
this._setupAuthImpersonation()
|
|
200
210
|
// 7. Wire modules that need orchestrator (phase 2 - kernel:ready signal)
|
|
201
211
|
this._wireModules()
|
|
202
212
|
// 8. Create EventRouter (needs signals + orchestrator)
|
|
203
213
|
this._createEventRouter()
|
|
204
214
|
// 9. Create SSEBridge (needs signals + authAdapter for token)
|
|
205
215
|
this._createSSEBridge()
|
|
206
|
-
this._setupSecurity()
|
|
207
216
|
this._createLayoutComponents()
|
|
208
217
|
this._createVueApp()
|
|
209
218
|
this._installPlugins()
|
|
@@ -230,6 +239,10 @@ export class Kernel {
|
|
|
230
239
|
this._createDeferredRegistry()
|
|
231
240
|
// 2. Create orchestrator early (modules need it for ctx.entity())
|
|
232
241
|
this._createOrchestrator()
|
|
242
|
+
// 2.5. Create PermissionRegistry early (modules register permissions via ctx.entity())
|
|
243
|
+
this._createPermissionRegistry()
|
|
244
|
+
// 2.6. Setup security early (SecurityModule needs ctx.security)
|
|
245
|
+
this._setupSecurity()
|
|
233
246
|
// 3. Register auth:ready deferred (if auth configured)
|
|
234
247
|
this._registerAuthDeferred()
|
|
235
248
|
// 4. Initialize legacy modules (can use all services, registers routes)
|
|
@@ -242,13 +255,14 @@ export class Kernel {
|
|
|
242
255
|
this._setupAuthGuard()
|
|
243
256
|
// 6. Setup auth:expired handler (needs router + authAdapter)
|
|
244
257
|
this._setupAuthExpiredHandler()
|
|
258
|
+
// 6.5. Setup auth impersonation (authAdapter reacts to signals)
|
|
259
|
+
this._setupAuthImpersonation()
|
|
245
260
|
// 7. Wire modules that need orchestrator (phase 2 - kernel:ready signal)
|
|
246
261
|
await this._wireModulesAsync()
|
|
247
262
|
// 8. Create EventRouter (needs signals + orchestrator)
|
|
248
263
|
this._createEventRouter()
|
|
249
264
|
// 9. Create SSEBridge (needs signals + authAdapter for token)
|
|
250
265
|
this._createSSEBridge()
|
|
251
|
-
this._setupSecurity()
|
|
252
266
|
this._createLayoutComponents()
|
|
253
267
|
this._createVueApp()
|
|
254
268
|
this._installPlugins()
|
|
@@ -327,6 +341,29 @@ export class Kernel {
|
|
|
327
341
|
})
|
|
328
342
|
}
|
|
329
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Setup authAdapter to react to impersonation signals
|
|
346
|
+
*
|
|
347
|
+
* If the authAdapter has a connectSignals() method, wire it up to the
|
|
348
|
+
* signal bus so it can react to auth:impersonate and auth:impersonate:stop
|
|
349
|
+
* signals automatically.
|
|
350
|
+
*
|
|
351
|
+
* This enables signal-driven impersonation: components emit signals,
|
|
352
|
+
* authAdapter updates its state in response.
|
|
353
|
+
*/
|
|
354
|
+
_setupAuthImpersonation() {
|
|
355
|
+
const { authAdapter } = this.options
|
|
356
|
+
if (!authAdapter?.connectSignals) return
|
|
357
|
+
|
|
358
|
+
const debug = this.options.debug ?? false
|
|
359
|
+
if (debug) {
|
|
360
|
+
console.debug('[Kernel] Wiring authAdapter.connectSignals() for impersonation')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Connect authAdapter to signals - returns cleanup function
|
|
364
|
+
this._authImpersonationCleanup = authAdapter.connectSignals(this.signals)
|
|
365
|
+
}
|
|
366
|
+
|
|
330
367
|
/**
|
|
331
368
|
* Fire entity cache warmups
|
|
332
369
|
* Fire-and-forget: pages that need cache will await via DeferredRegistry.
|
|
@@ -467,15 +504,19 @@ export class Kernel {
|
|
|
467
504
|
* Some modules may need access to the orchestrator after it's created.
|
|
468
505
|
* This method emits 'kernel:ready' signal that modules can listen to.
|
|
469
506
|
*
|
|
507
|
+
* Note: We don't pass kernel/orchestrator in the payload to avoid cyclic
|
|
508
|
+
* reference errors when debug logging is enabled. Handlers can access
|
|
509
|
+
* these via their stored context from connect().
|
|
510
|
+
*
|
|
470
511
|
* @private
|
|
471
512
|
*/
|
|
472
513
|
_wireModules() {
|
|
473
514
|
if (!this.moduleLoader) return
|
|
474
515
|
|
|
475
|
-
// Emit kernel:ready signal
|
|
516
|
+
// Emit kernel:ready signal - payload is intentionally minimal to avoid
|
|
517
|
+
// cyclic reference errors when QuarKernel debug mode serializes events
|
|
476
518
|
const result = this.signals.emit('kernel:ready', {
|
|
477
|
-
|
|
478
|
-
kernel: this
|
|
519
|
+
ready: true
|
|
479
520
|
})
|
|
480
521
|
|
|
481
522
|
// If emit returns a promise (async handlers), we can't await it
|
|
@@ -495,10 +536,10 @@ export class Kernel {
|
|
|
495
536
|
async _wireModulesAsync() {
|
|
496
537
|
if (!this.moduleLoader) return
|
|
497
538
|
|
|
498
|
-
// Emit kernel:ready signal
|
|
539
|
+
// Emit kernel:ready signal - payload is intentionally minimal to avoid
|
|
540
|
+
// cyclic reference errors when QuarKernel debug mode serializes events
|
|
499
541
|
await this.signals.emit('kernel:ready', {
|
|
500
|
-
|
|
501
|
-
kernel: this
|
|
542
|
+
ready: true
|
|
502
543
|
})
|
|
503
544
|
}
|
|
504
545
|
|
|
@@ -692,6 +733,35 @@ export class Kernel {
|
|
|
692
733
|
})
|
|
693
734
|
}
|
|
694
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Create PermissionRegistry early so modules can register permissions
|
|
738
|
+
* via ctx.entity() and ctx.permissions()
|
|
739
|
+
*/
|
|
740
|
+
_createPermissionRegistry() {
|
|
741
|
+
this.permissionRegistry = new PermissionRegistry()
|
|
742
|
+
|
|
743
|
+
// Register core system permissions
|
|
744
|
+
this._registerCorePermissions()
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Register core system permissions provided by the framework
|
|
749
|
+
* These are always available regardless of which modules are loaded
|
|
750
|
+
*/
|
|
751
|
+
_registerCorePermissions() {
|
|
752
|
+
// Auth permissions - for impersonation and auth management
|
|
753
|
+
this.permissionRegistry.register('auth', {
|
|
754
|
+
'impersonate': 'Impersonate other users',
|
|
755
|
+
'manage': 'Manage authentication settings'
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
// Admin permissions - for system administration
|
|
759
|
+
this.permissionRegistry.register('admin', {
|
|
760
|
+
'access': 'Access admin panel',
|
|
761
|
+
'config': 'Edit system configuration'
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
695
765
|
/**
|
|
696
766
|
* Setup security layer (role hierarchy, permissions)
|
|
697
767
|
*
|
|
@@ -712,12 +782,28 @@ export class Kernel {
|
|
|
712
782
|
*/
|
|
713
783
|
_setupSecurity() {
|
|
714
784
|
const { security, entityAuthAdapter } = this.options
|
|
785
|
+
|
|
786
|
+
// PermissionRegistry should already be created by _createPermissionRegistry()
|
|
787
|
+
// but create it here as fallback for backward compatibility
|
|
788
|
+
if (!this.permissionRegistry) {
|
|
789
|
+
this.permissionRegistry = new PermissionRegistry()
|
|
790
|
+
}
|
|
791
|
+
|
|
715
792
|
if (!security) return
|
|
716
793
|
|
|
717
|
-
//
|
|
794
|
+
// Resolve roleGranter: explicit adapter or auto-create from config
|
|
795
|
+
let roleGranter = security.roleGranter
|
|
796
|
+
if (!roleGranter && (security.role_permissions || security.role_hierarchy)) {
|
|
797
|
+
roleGranter = new StaticRoleGranterAdapter({
|
|
798
|
+
role_hierarchy: security.role_hierarchy || {},
|
|
799
|
+
role_permissions: security.role_permissions || {},
|
|
800
|
+
role_labels: security.role_labels || {}
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Create SecurityChecker with roleGranter
|
|
718
805
|
this.securityChecker = createSecurityChecker({
|
|
719
|
-
|
|
720
|
-
role_permissions: security.role_permissions || {},
|
|
806
|
+
roleGranter,
|
|
721
807
|
getCurrentUser: () => entityAuthAdapter?.getCurrentUser?.() || null
|
|
722
808
|
})
|
|
723
809
|
|
|
@@ -728,6 +814,16 @@ export class Kernel {
|
|
|
728
814
|
if (entityAuthAdapter?.setSecurityChecker) {
|
|
729
815
|
entityAuthAdapter.setSecurityChecker(this.securityChecker)
|
|
730
816
|
}
|
|
817
|
+
|
|
818
|
+
// Install roleGranter (for EntityRoleGranterAdapter signal subscriptions)
|
|
819
|
+
if (roleGranter?.install) {
|
|
820
|
+
// Create minimal context for roleGranter installation
|
|
821
|
+
const ctx = {
|
|
822
|
+
orchestrator: this.orchestrator,
|
|
823
|
+
signals: this.signals
|
|
824
|
+
}
|
|
825
|
+
roleGranter.install(ctx)
|
|
826
|
+
}
|
|
731
827
|
}
|
|
732
828
|
|
|
733
829
|
/**
|
|
@@ -839,19 +935,26 @@ export class Kernel {
|
|
|
839
935
|
throw new Error('[Kernel] root component is required')
|
|
840
936
|
}
|
|
841
937
|
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
938
|
+
// Always wrap root with Toast (and DebugBar if enabled)
|
|
939
|
+
const OriginalRoot = this.options.root
|
|
940
|
+
const DebugBarComponent = this.options.debugBar?.component && QdadmDebugBar ? QdadmDebugBar : null
|
|
941
|
+
|
|
942
|
+
// Wrap root with DebugBar if enabled
|
|
943
|
+
// Note: Toast must be included by apps in their App.vue for pages outside BaseLayout
|
|
944
|
+
if (DebugBarComponent) {
|
|
945
|
+
const WrappedRoot = defineComponent({
|
|
847
946
|
name: 'QdadmRootWrapper',
|
|
848
|
-
|
|
849
|
-
|
|
947
|
+
components: { OriginalRoot, DebugBarComponent },
|
|
948
|
+
setup() {
|
|
949
|
+
return () => h('div', { id: 'qdadm-root', style: 'display: contents' }, [
|
|
950
|
+
h(OriginalRoot),
|
|
951
|
+
h(DebugBarComponent)
|
|
952
|
+
])
|
|
850
953
|
}
|
|
851
|
-
}
|
|
954
|
+
})
|
|
852
955
|
this.vueApp = createApp(WrappedRoot)
|
|
853
956
|
} else {
|
|
854
|
-
this.vueApp = createApp(
|
|
957
|
+
this.vueApp = createApp(OriginalRoot)
|
|
855
958
|
}
|
|
856
959
|
}
|
|
857
960
|
|
|
@@ -941,6 +1044,14 @@ export class Kernel {
|
|
|
941
1044
|
// Layout components injection for useLayoutResolver
|
|
942
1045
|
app.provide('qdadmLayoutComponents', this.layoutComponents)
|
|
943
1046
|
|
|
1047
|
+
// Security injection (for SecurityModule and other system modules)
|
|
1048
|
+
if (this.securityChecker) {
|
|
1049
|
+
app.provide('qdadmSecurityChecker', this.securityChecker)
|
|
1050
|
+
}
|
|
1051
|
+
if (this.permissionRegistry) {
|
|
1052
|
+
app.provide('qdadmPermissionRegistry', this.permissionRegistry)
|
|
1053
|
+
}
|
|
1054
|
+
|
|
944
1055
|
// qdadm plugin
|
|
945
1056
|
// Note: Don't pass managers here - orchestrator already has resolved managers
|
|
946
1057
|
// from createManagers(). Passing raw configs would overwrite them.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { managerFactory } from '../entity/factory.js'
|
|
28
28
|
import { registry } from '../module/moduleRegistry.js'
|
|
29
|
+
import { UsersManager } from '../security/UsersManager.js'
|
|
29
30
|
|
|
30
31
|
export class KernelContext {
|
|
31
32
|
/**
|
|
@@ -129,6 +130,22 @@ export class KernelContext {
|
|
|
129
130
|
return this._kernel.securityChecker
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Get permission registry
|
|
135
|
+
* @returns {import('../security/PermissionRegistry.js').PermissionRegistry|null}
|
|
136
|
+
*/
|
|
137
|
+
get permissionRegistry() {
|
|
138
|
+
return this._kernel.permissionRegistry
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get auth adapter shortcut
|
|
143
|
+
* @returns {object|null}
|
|
144
|
+
*/
|
|
145
|
+
get auth() {
|
|
146
|
+
return this._kernel.options?.authAdapter ?? null
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
150
|
// Fluent registration methods (return this for chaining)
|
|
134
151
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -167,6 +184,73 @@ export class KernelContext {
|
|
|
167
184
|
this._kernel.orchestrator.register(name, manager)
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
// Auto-register CRUD permissions for this entity
|
|
188
|
+
if (this._kernel.permissionRegistry) {
|
|
189
|
+
this._kernel.permissionRegistry.registerEntity(name, {
|
|
190
|
+
module: this._module?.constructor?.name || 'unknown',
|
|
191
|
+
// Register entity-own:* permissions if manager has isOwn configured
|
|
192
|
+
hasOwnership: !!manager._isOwn
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return this
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Register a users entity with standard fields and role linking
|
|
201
|
+
*
|
|
202
|
+
* Creates a UsersManager with:
|
|
203
|
+
* - username, password, role fields (role linked to roles entity)
|
|
204
|
+
* - System entity flag
|
|
205
|
+
* - Admin-only access by default
|
|
206
|
+
*
|
|
207
|
+
* @param {Object} options - Configuration options
|
|
208
|
+
* @param {Object} options.storage - Storage adapter (required)
|
|
209
|
+
* @param {Object} [options.extraFields={}] - Additional fields beyond username/password/role
|
|
210
|
+
* @param {string} [options.adminRole='ROLE_ADMIN'] - Role required for user management
|
|
211
|
+
* @param {boolean} [options.adminOnly=true] - Restrict access to admin role only
|
|
212
|
+
* @param {Object} [options.fieldOverrides={}] - Override default field configs
|
|
213
|
+
* @returns {this}
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* // Basic usage with MockApiStorage
|
|
217
|
+
* ctx.userEntity({
|
|
218
|
+
* storage: new MockApiStorage({ entityName: 'users', initialData: usersFixture })
|
|
219
|
+
* })
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* // With extra fields and API storage
|
|
223
|
+
* ctx.userEntity({
|
|
224
|
+
* storage: new ApiStorage({ endpoint: '/api/users' }),
|
|
225
|
+
* extraFields: {
|
|
226
|
+
* email: { type: 'email', label: 'Email', required: true },
|
|
227
|
+
* firstName: { type: 'text', label: 'First Name' },
|
|
228
|
+
* lastName: { type: 'text', label: 'Last Name' }
|
|
229
|
+
* }
|
|
230
|
+
* })
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* // Allow non-admin access (e.g., for user profile editing)
|
|
234
|
+
* ctx.userEntity({
|
|
235
|
+
* storage: usersStorage,
|
|
236
|
+
* adminOnly: false
|
|
237
|
+
* })
|
|
238
|
+
*/
|
|
239
|
+
userEntity(options = {}) {
|
|
240
|
+
const manager = new UsersManager(options)
|
|
241
|
+
|
|
242
|
+
// Register with orchestrator
|
|
243
|
+
if (this._kernel.orchestrator) {
|
|
244
|
+
this._kernel.orchestrator.register('users', manager)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Auto-register CRUD permissions for users entity
|
|
248
|
+
if (this._kernel.permissionRegistry) {
|
|
249
|
+
this._kernel.permissionRegistry.registerEntity('users', {
|
|
250
|
+
module: this._module?.constructor?.name || 'unknown'
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
170
254
|
return this
|
|
171
255
|
}
|
|
172
256
|
|
|
@@ -521,6 +605,80 @@ export class KernelContext {
|
|
|
521
605
|
}
|
|
522
606
|
return this
|
|
523
607
|
}
|
|
608
|
+
|
|
609
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
+
// Permission registration
|
|
611
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Register permissions for this module
|
|
615
|
+
*
|
|
616
|
+
* Permissions are namespaced and collected in the central PermissionRegistry.
|
|
617
|
+
* Use this to declare what permissions your module provides.
|
|
618
|
+
*
|
|
619
|
+
* Permission format: namespace:action
|
|
620
|
+
* - For entity CRUD: automatically prefixed with 'entity:'
|
|
621
|
+
* - For system features: use any namespace (auth:, admin:, feature:, etc.)
|
|
622
|
+
*
|
|
623
|
+
* @param {string} namespace - Permission namespace (e.g., 'books', 'auth', 'admin:config')
|
|
624
|
+
* @param {Object<string, string|PermissionMeta>} permissions - Permission definitions
|
|
625
|
+
* @param {Object} [options={}]
|
|
626
|
+
* @param {boolean} [options.isEntity=false] - Auto-prefix with 'entity:'
|
|
627
|
+
* @returns {this}
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* // Entity custom permissions (auto-prefixed with 'entity:')
|
|
631
|
+
* ctx.permissions('books', {
|
|
632
|
+
* checkout: 'Checkout a book',
|
|
633
|
+
* reserve: { label: 'Reserve', description: 'Reserve a book for later' }
|
|
634
|
+
* }, { isEntity: true })
|
|
635
|
+
* // → entity:books:checkout, entity:books:reserve
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* // System permissions (no prefix)
|
|
639
|
+
* ctx.permissions('auth', {
|
|
640
|
+
* impersonate: 'Impersonate another user',
|
|
641
|
+
* 'manage-roles': 'Manage user roles'
|
|
642
|
+
* })
|
|
643
|
+
* // → auth:impersonate, auth:manage-roles
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* // Admin permissions with nested namespace
|
|
647
|
+
* ctx.permissions('admin:config', {
|
|
648
|
+
* view: 'View configuration',
|
|
649
|
+
* edit: 'Edit configuration'
|
|
650
|
+
* })
|
|
651
|
+
* // → admin:config:view, admin:config:edit
|
|
652
|
+
*/
|
|
653
|
+
permissions(namespace, permissions, options = {}) {
|
|
654
|
+
if (this._kernel.permissionRegistry) {
|
|
655
|
+
this._kernel.permissionRegistry.register(namespace, permissions, {
|
|
656
|
+
...options,
|
|
657
|
+
module: this._module?.constructor?.name || 'unknown'
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
return this
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Register entity permissions (convenience method)
|
|
665
|
+
*
|
|
666
|
+
* Shorthand for registering entity-namespaced permissions.
|
|
667
|
+
* Equivalent to: ctx.permissions(entity, perms, { isEntity: true })
|
|
668
|
+
*
|
|
669
|
+
* @param {string} entity - Entity name
|
|
670
|
+
* @param {Object<string, string|PermissionMeta>} permissions - Permission definitions
|
|
671
|
+
* @returns {this}
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* ctx.entityPermissions('books', {
|
|
675
|
+
* checkout: 'Checkout a book'
|
|
676
|
+
* })
|
|
677
|
+
* // → entity:books:checkout
|
|
678
|
+
*/
|
|
679
|
+
entityPermissions(entity, permissions) {
|
|
680
|
+
return this.permissions(entity, permissions, { isEntity: true })
|
|
681
|
+
}
|
|
524
682
|
}
|
|
525
683
|
|
|
526
684
|
/**
|