qdadm 0.35.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 +199 -31
- 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 +159 -27
- package/src/debug/components/panels/EntitiesPanel.vue +18 -2
- package/src/entity/EntityManager.js +205 -36
- 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 +135 -25
- package/src/kernel/KernelContext.js +166 -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
|
@@ -59,9 +59,12 @@ export class EntityManager {
|
|
|
59
59
|
localFilterThreshold = null, // Items threshold to switch to local filtering (null = use default)
|
|
60
60
|
readOnly = false, // If true, canCreate/canUpdate/canDelete return false
|
|
61
61
|
warmup = true, // If true, cache is preloaded at boot via DeferredRegistry
|
|
62
|
-
authSensitive
|
|
62
|
+
authSensitive, // If true, auto-invalidate datalayer on auth events (auto-inferred from storage.requiresAuth if not set)
|
|
63
|
+
system = false, // If true, marks entity as system-provided (roles, users)
|
|
63
64
|
// Scope control
|
|
64
65
|
scopeWhitelist = null, // Array of scopes/modules that can bypass restrictions
|
|
66
|
+
// Ownership (for record-level access control)
|
|
67
|
+
isOwn = null, // (record, user) => boolean - check if user owns the record
|
|
65
68
|
// Relations
|
|
66
69
|
children = {}, // { roles: { entity: 'roles', endpoint?: ':id/roles' } }
|
|
67
70
|
parent = null, // { entity: 'users', foreignKey: 'user_id' }
|
|
@@ -86,11 +89,16 @@ export class EntityManager {
|
|
|
86
89
|
this.localFilterThreshold = localFilterThreshold
|
|
87
90
|
this._readOnly = readOnly
|
|
88
91
|
this._warmup = warmup
|
|
89
|
-
|
|
92
|
+
// Auto-infer authSensitive from storage.requiresAuth if not explicitly set
|
|
93
|
+
this._authSensitive = authSensitive ?? this._getStorageRequiresAuth()
|
|
94
|
+
this._system = system
|
|
90
95
|
|
|
91
96
|
// Scope control
|
|
92
97
|
this._scopeWhitelist = scopeWhitelist
|
|
93
98
|
|
|
99
|
+
// Ownership
|
|
100
|
+
this._isOwn = isOwn
|
|
101
|
+
|
|
94
102
|
// Relations
|
|
95
103
|
this._children = children
|
|
96
104
|
this._parent = parent
|
|
@@ -367,48 +375,54 @@ export class EntityManager {
|
|
|
367
375
|
}
|
|
368
376
|
|
|
369
377
|
/**
|
|
370
|
-
* Build permission string for an action
|
|
378
|
+
* Build permission string for an entity action
|
|
379
|
+
*
|
|
380
|
+
* Format: entity:{name}:{action}
|
|
381
|
+
* Examples: entity:books:read, entity:users:create
|
|
371
382
|
*
|
|
372
|
-
*
|
|
373
|
-
* -
|
|
374
|
-
* -
|
|
375
|
-
* -
|
|
383
|
+
* Role permissions can use wildcards to match:
|
|
384
|
+
* - entity:*:read → read any entity
|
|
385
|
+
* - entity:books:* → any action on books
|
|
386
|
+
* - entity:** → all entity permissions
|
|
376
387
|
*
|
|
377
388
|
* @param {string} action - Action name (read, create, update, delete, list)
|
|
378
389
|
* @returns {string} - Permission string
|
|
379
390
|
* @private
|
|
380
391
|
*/
|
|
381
392
|
_getPermissionString(action) {
|
|
382
|
-
|
|
383
|
-
if (!checker) return `entity:${action}`
|
|
384
|
-
|
|
385
|
-
const config = checker.entityPermissions
|
|
386
|
-
if (config === false) return `entity:${action}`
|
|
387
|
-
if (config === true) return `${this.name}:${action}`
|
|
388
|
-
if (Array.isArray(config) && config.includes(this.name)) {
|
|
389
|
-
return `${this.name}:${action}`
|
|
390
|
-
}
|
|
391
|
-
return `entity:${action}`
|
|
393
|
+
return `entity:${this.name}:${action}`
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
/**
|
|
395
|
-
*
|
|
397
|
+
* Get the current authenticated user
|
|
396
398
|
*
|
|
397
|
-
*
|
|
398
|
-
* This method respects the entity_permissions config for granular permissions.
|
|
399
|
+
* Tries authAdapter.getCurrentUser() first, then falls back to kernel's authAdapter.
|
|
399
400
|
*
|
|
400
|
-
* @
|
|
401
|
-
* @
|
|
402
|
-
* @returns {boolean}
|
|
401
|
+
* @returns {object|null} Current user or null
|
|
402
|
+
* @private
|
|
403
403
|
*/
|
|
404
|
-
|
|
405
|
-
//
|
|
406
|
-
if (this.authAdapter
|
|
407
|
-
|
|
408
|
-
|
|
404
|
+
_getCurrentUser() {
|
|
405
|
+
// Try authAdapter.getCurrentUser() first (EntityAuthAdapter subclass)
|
|
406
|
+
if (typeof this.authAdapter?.getCurrentUser === 'function') {
|
|
407
|
+
try {
|
|
408
|
+
return this.authAdapter.getCurrentUser()
|
|
409
|
+
} catch {
|
|
410
|
+
// Fallback if not implemented
|
|
411
|
+
}
|
|
409
412
|
}
|
|
410
|
-
// Fallback to
|
|
411
|
-
return this.
|
|
413
|
+
// Fallback to kernel's authAdapter
|
|
414
|
+
return this._orchestrator?.kernel?.options?.authAdapter?.getUser?.() ?? null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check if SecurityChecker is configured via authAdapter
|
|
419
|
+
* Only returns true when adapter has _securityChecker set,
|
|
420
|
+
* allowing legacy canPerform() adapters to work correctly.
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
* @private
|
|
423
|
+
*/
|
|
424
|
+
_hasSecurityChecker() {
|
|
425
|
+
return this.authAdapter?._securityChecker != null
|
|
412
426
|
}
|
|
413
427
|
|
|
414
428
|
/**
|
|
@@ -423,9 +437,22 @@ export class EntityManager {
|
|
|
423
437
|
* Check if the current user can perform an action, optionally on a specific record
|
|
424
438
|
*
|
|
425
439
|
* This is the primary permission check method. It combines:
|
|
426
|
-
* 1. Local restrictions (readOnly
|
|
427
|
-
* 2.
|
|
428
|
-
* 3.
|
|
440
|
+
* 1. Local restrictions (readOnly)
|
|
441
|
+
* 2. Ownership check via isOwn callback (if configured)
|
|
442
|
+
* 3. Permission check via isGranted() (if SecurityChecker configured)
|
|
443
|
+
* OR legacy canPerform()/canAccessRecord() fallback
|
|
444
|
+
*
|
|
445
|
+
* Permission format: entity:{name}:{action}
|
|
446
|
+
* Wildcard examples:
|
|
447
|
+
* - entity:*:read → can read any entity
|
|
448
|
+
* - entity:books:* → any action on books
|
|
449
|
+
* - entity:** → all entity permissions
|
|
450
|
+
*
|
|
451
|
+
* Ownership pattern:
|
|
452
|
+
* - Configure isOwn callback: (record, user) => boolean
|
|
453
|
+
* - When user owns a record, check entity-own:{entity}:{action} permission
|
|
454
|
+
* - Example: entity-own:loans:update allows owner to update their loans
|
|
455
|
+
* - Use entity-own:{entity}:** to allow all actions on owned records
|
|
429
456
|
*
|
|
430
457
|
* @param {string} action - Action to check: 'read', 'create', 'update', 'delete', 'list'
|
|
431
458
|
* @param {object} [record] - Optional: specific record to check (for silo validation)
|
|
@@ -441,6 +468,16 @@ export class EntityManager {
|
|
|
441
468
|
* manager.canAccess('read', item) // Can user see this specific item?
|
|
442
469
|
* manager.canAccess('update', item) // Can user edit this specific item?
|
|
443
470
|
* manager.canAccess('delete', item) // Can user delete this specific item?
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* // Ownership pattern with permissions
|
|
474
|
+
* const loansManager = new EntityManager({
|
|
475
|
+
* name: 'loans',
|
|
476
|
+
* isOwn: (record, user) => record.user_id === user?.id,
|
|
477
|
+
* storage: loansStorage
|
|
478
|
+
* })
|
|
479
|
+
* // Role config: { permissions: ['entity-own:loans:**'] }
|
|
480
|
+
* loansManager.canAccess('update', myLoan) // true if I own the loan
|
|
444
481
|
*/
|
|
445
482
|
canAccess(action, record = null) {
|
|
446
483
|
// 1. Check readOnly restriction for write actions
|
|
@@ -448,13 +485,31 @@ export class EntityManager {
|
|
|
448
485
|
return false
|
|
449
486
|
}
|
|
450
487
|
|
|
451
|
-
// 2.
|
|
488
|
+
// 2. Ownership check: if user owns the record, check entity-own permission
|
|
489
|
+
if (record && this._isOwn && this._hasSecurityChecker()) {
|
|
490
|
+
const user = this._getCurrentUser()
|
|
491
|
+
if (user && this._isOwn(record, user)) {
|
|
492
|
+
// Owner - check entity-own:{entity}:{action} permission
|
|
493
|
+
const ownPerm = `entity-own:${this.name}:${action}`
|
|
494
|
+
if (this.authAdapter.isGranted(ownPerm, record)) {
|
|
495
|
+
return true
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 3. Use isGranted() with entity:name:action format when available
|
|
501
|
+
if (this._hasSecurityChecker()) {
|
|
502
|
+
const perm = this._getPermissionString(action)
|
|
503
|
+
return this.authAdapter.isGranted(perm, record)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 4. Legacy fallback: canPerform() + canAccessRecord()
|
|
452
507
|
const canPerformAction = this.authAdapter.canPerform(this.name, action)
|
|
453
508
|
if (!canPerformAction) {
|
|
454
509
|
return false
|
|
455
510
|
}
|
|
456
511
|
|
|
457
|
-
//
|
|
512
|
+
// 5. Silo check: if record provided, can user access this specific record?
|
|
458
513
|
if (record !== null) {
|
|
459
514
|
return this.authAdapter.canAccessRecord(this.name, record)
|
|
460
515
|
}
|
|
@@ -484,6 +539,14 @@ export class EntityManager {
|
|
|
484
539
|
return this._readOnly
|
|
485
540
|
}
|
|
486
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Check if entity is system-provided (roles, users)
|
|
544
|
+
* @returns {boolean}
|
|
545
|
+
*/
|
|
546
|
+
get system() {
|
|
547
|
+
return this._system
|
|
548
|
+
}
|
|
549
|
+
|
|
487
550
|
/**
|
|
488
551
|
* Check if user can create new entities
|
|
489
552
|
* Delegates to canAccess('create')
|
|
@@ -642,6 +705,91 @@ export class EntityManager {
|
|
|
642
705
|
.map(([name, config]) => ({ name, ...config }))
|
|
643
706
|
}
|
|
644
707
|
|
|
708
|
+
// ============ REFERENCE OPTIONS ============
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Resolve reference options for a field
|
|
712
|
+
*
|
|
713
|
+
* If the field has a `reference` property, fetches data from the referenced
|
|
714
|
+
* entity and returns options array for select/dropdown.
|
|
715
|
+
*
|
|
716
|
+
* @param {string} fieldName - Field name
|
|
717
|
+
* @returns {Promise<Array<{label: string, value: any}>>} - Options array
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* // Field config: { type: 'select', reference: { entity: 'roles', labelField: 'label' } }
|
|
721
|
+
* const options = await manager.resolveReferenceOptions('role')
|
|
722
|
+
* // Returns: [{ label: 'Admin', value: 'ROLE_ADMIN' }, { label: 'User', value: 'ROLE_USER' }]
|
|
723
|
+
*/
|
|
724
|
+
async resolveReferenceOptions(fieldName) {
|
|
725
|
+
const fieldConfig = this._fields[fieldName]
|
|
726
|
+
if (!fieldConfig) {
|
|
727
|
+
console.warn(`[EntityManager:${this.name}] Unknown field '${fieldName}'`)
|
|
728
|
+
return []
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// If field has static options, return them
|
|
732
|
+
if (fieldConfig.options && !fieldConfig.reference) {
|
|
733
|
+
return fieldConfig.options
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// If no reference, return empty
|
|
737
|
+
if (!fieldConfig.reference) {
|
|
738
|
+
return []
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Need orchestrator to access other managers
|
|
742
|
+
if (!this._orchestrator) {
|
|
743
|
+
console.warn(`[EntityManager:${this.name}] No orchestrator, cannot resolve reference for '${fieldName}'`)
|
|
744
|
+
return fieldConfig.options || []
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const { entity, labelField, valueField } = fieldConfig.reference
|
|
748
|
+
const refManager = this._orchestrator.get(entity)
|
|
749
|
+
|
|
750
|
+
if (!refManager) {
|
|
751
|
+
console.warn(`[EntityManager:${this.name}] Referenced entity '${entity}' not found`)
|
|
752
|
+
return fieldConfig.options || []
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
// Fetch all items from referenced entity
|
|
757
|
+
const { items } = await refManager.list({ limit: 1000 })
|
|
758
|
+
|
|
759
|
+
// Build options array
|
|
760
|
+
const refLabelField = labelField || refManager.labelField || 'label'
|
|
761
|
+
const refValueField = valueField || refManager.idField || 'id'
|
|
762
|
+
|
|
763
|
+
return items.map(item => ({
|
|
764
|
+
label: item[refLabelField] ?? item[refValueField],
|
|
765
|
+
value: item[refValueField]
|
|
766
|
+
}))
|
|
767
|
+
} catch (error) {
|
|
768
|
+
console.error(`[EntityManager:${this.name}] Failed to resolve reference for '${fieldName}':`, error)
|
|
769
|
+
return fieldConfig.options || []
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Resolve all reference options for form fields
|
|
775
|
+
*
|
|
776
|
+
* Returns a map of fieldName -> options for all fields with references.
|
|
777
|
+
*
|
|
778
|
+
* @returns {Promise<Map<string, Array<{label: string, value: any}>>>}
|
|
779
|
+
*/
|
|
780
|
+
async resolveAllReferenceOptions() {
|
|
781
|
+
const optionsMap = new Map()
|
|
782
|
+
|
|
783
|
+
for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
|
|
784
|
+
if (fieldConfig.reference) {
|
|
785
|
+
const options = await this.resolveReferenceOptions(fieldName)
|
|
786
|
+
optionsMap.set(fieldName, options)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return optionsMap
|
|
791
|
+
}
|
|
792
|
+
|
|
645
793
|
// ============ SEVERITY MAPS ============
|
|
646
794
|
|
|
647
795
|
/**
|
|
@@ -1034,6 +1182,25 @@ export class EntityManager {
|
|
|
1034
1182
|
return caps.supportsTotal ?? false
|
|
1035
1183
|
}
|
|
1036
1184
|
|
|
1185
|
+
/**
|
|
1186
|
+
* Check if storage requires authentication
|
|
1187
|
+
*
|
|
1188
|
+
* Used to auto-infer authSensitive when not explicitly set.
|
|
1189
|
+
* Checks both instance capabilities and static capabilities.
|
|
1190
|
+
*
|
|
1191
|
+
* @returns {boolean} - true if storage requires auth
|
|
1192
|
+
* @private
|
|
1193
|
+
*/
|
|
1194
|
+
_getStorageRequiresAuth() {
|
|
1195
|
+
// Check instance capabilities first (may have dynamic requiresAuth)
|
|
1196
|
+
if (this.storage?.capabilities?.requiresAuth !== undefined) {
|
|
1197
|
+
return this.storage.capabilities.requiresAuth
|
|
1198
|
+
}
|
|
1199
|
+
// Fallback to static capabilities
|
|
1200
|
+
const caps = this.storage?.constructor?.capabilities || {}
|
|
1201
|
+
return caps.requiresAuth ?? false
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1037
1204
|
/**
|
|
1038
1205
|
* Get searchable fields declared by storage adapter
|
|
1039
1206
|
*
|
|
@@ -1243,7 +1410,9 @@ export class EntityManager {
|
|
|
1243
1410
|
*/
|
|
1244
1411
|
get isCacheEnabled() {
|
|
1245
1412
|
if (this.effectiveThreshold <= 0) return false
|
|
1246
|
-
|
|
1413
|
+
// Check capabilities (instance getter or static)
|
|
1414
|
+
const caps = this.storage?.capabilities || this.storage?.constructor?.capabilities
|
|
1415
|
+
if (caps?.supportsCaching === false) return false
|
|
1247
1416
|
if (!this.storageSupportsTotal) return false
|
|
1248
1417
|
return true
|
|
1249
1418
|
}
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EntityAuthAdapter -
|
|
2
|
+
* EntityAuthAdapter - Thin layer for entity-level permission checks
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides entity-level permission checking by delegating to SecurityChecker.
|
|
5
|
+
* All methods have sensible defaults - subclass only if you need custom behavior.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Default behavior (when SecurityChecker is configured):
|
|
8
|
+
* - canPerform() → isGranted('entity:{entity}:{action}')
|
|
9
|
+
* - canAccessRecord() → isGranted('entity:{entity}:read', record)
|
|
10
|
+
* - getCurrentUser() → uses callback if provided, null otherwise
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* Example: Can user see invoice #123? (ownership, team membership, etc.)
|
|
12
|
-
*
|
|
13
|
-
* Note: For user session authentication (login/logout), see SessionAuthAdapter.
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
12
|
+
* Usage patterns:
|
|
16
13
|
* ```js
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* }
|
|
22
|
-
*
|
|
23
|
-
* canAccessRecord(entity, record) {
|
|
24
|
-
* const user = this.getCurrentUser()
|
|
25
|
-
* return record.owner_id === user?.id || record.team_id === user?.team_id
|
|
26
|
-
* }
|
|
14
|
+
* // 1. Callback-based (simplest - no subclass needed)
|
|
15
|
+
* const adapter = new EntityAuthAdapter({
|
|
16
|
+
* getCurrentUser: () => myAuthStore.user
|
|
17
|
+
* })
|
|
27
18
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
19
|
+
* // 2. Subclass-based (for custom permission logic)
|
|
20
|
+
* class MyAdapter extends EntityAuthAdapter {
|
|
21
|
+
* canPerform(entity, action) {
|
|
22
|
+
* if (['orders', 'invoices'].includes(entity) && !this.getCurrentUser()) {
|
|
23
|
+
* return false
|
|
24
|
+
* }
|
|
25
|
+
* return super.canPerform(entity, action)
|
|
30
26
|
* }
|
|
31
27
|
* }
|
|
32
28
|
* ```
|
|
33
|
-
*
|
|
34
|
-
* @interface
|
|
35
29
|
*/
|
|
36
30
|
export class EntityAuthAdapter {
|
|
37
31
|
/**
|
|
@@ -41,6 +35,23 @@ export class EntityAuthAdapter {
|
|
|
41
35
|
*/
|
|
42
36
|
_securityChecker = null
|
|
43
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Callback to get current user (alternative to subclassing)
|
|
40
|
+
* @type {Function|null}
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
_getCurrentUserCallback = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {Function} [options.getCurrentUser] - Callback that returns current user or null
|
|
48
|
+
*/
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
if (options.getCurrentUser) {
|
|
51
|
+
this._getCurrentUserCallback = options.getCurrentUser
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
/**
|
|
45
56
|
* Set the SecurityChecker instance for isGranted() delegation
|
|
46
57
|
*
|
|
@@ -62,7 +73,7 @@ export class EntityAuthAdapter {
|
|
|
62
73
|
*
|
|
63
74
|
* @example
|
|
64
75
|
* adapter.isGranted('ROLE_ADMIN') // Check role
|
|
65
|
-
* adapter.isGranted('entity:delete')
|
|
76
|
+
* adapter.isGranted('entity:books:delete') // Check permission
|
|
66
77
|
* adapter.isGranted('books:delete', book) // Check with subject
|
|
67
78
|
*/
|
|
68
79
|
isGranted(attribute, subject = null) {
|
|
@@ -73,8 +84,6 @@ export class EntityAuthAdapter {
|
|
|
73
84
|
/**
|
|
74
85
|
* Check if current user can assign a specific role
|
|
75
86
|
*
|
|
76
|
-
* Uses SecurityChecker's canAssignRole if available.
|
|
77
|
-
*
|
|
78
87
|
* @param {string} targetRole - Role to assign
|
|
79
88
|
* @returns {boolean} True if user can assign this role
|
|
80
89
|
*/
|
|
@@ -96,50 +105,49 @@ export class EntityAuthAdapter {
|
|
|
96
105
|
/**
|
|
97
106
|
* Check if the current user can perform an action on an entity type (scope check)
|
|
98
107
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* record they want to act on.
|
|
108
|
+
* Default: delegates to isGranted('entity:{entity}:{action}')
|
|
109
|
+
* Override for custom authentication requirements or business rules.
|
|
102
110
|
*
|
|
103
111
|
* @param {string} entity - Entity name (e.g., 'users', 'invoices', 'products')
|
|
104
112
|
* @param {string} action - Action to check: 'read', 'create', 'update', 'delete', 'list'
|
|
105
113
|
* @returns {boolean} True if user can perform the action on this entity type
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* adapter.canPerform('invoices', 'create') // true/false
|
|
109
|
-
* adapter.canPerform('users', 'delete') // true/false
|
|
110
114
|
*/
|
|
111
115
|
canPerform(entity, action) {
|
|
112
|
-
|
|
116
|
+
return this.isGranted(`entity:${entity}:${action}`)
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
120
|
* Check if the current user can access a specific record (silo check)
|
|
117
121
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* ownership, team membership, or other business rules.
|
|
122
|
+
* Default: delegates to isGranted('entity:{entity}:read', record)
|
|
123
|
+
* Note: Ownership checks are typically handled via EntityManager's isOwn callback.
|
|
121
124
|
*
|
|
122
125
|
* @param {string} entity - Entity name (e.g., 'users', 'invoices')
|
|
123
126
|
* @param {object} record - The full entity record to check access for
|
|
124
127
|
* @returns {boolean} True if user can access this specific record
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* adapter.canAccessRecord('invoices', { id: 123, owner_id: 456, ... })
|
|
128
128
|
*/
|
|
129
129
|
canAccessRecord(entity, record) {
|
|
130
|
-
|
|
130
|
+
return this.isGranted(`entity:${entity}:read`, record)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Get the current authenticated user
|
|
135
135
|
*
|
|
136
|
-
* Returns
|
|
137
|
-
*
|
|
136
|
+
* Returns user from:
|
|
137
|
+
* 1. _getCurrentUserCallback if provided in constructor
|
|
138
|
+
* 2. Subclass override if using class extension pattern
|
|
139
|
+
* 3. null if neither is configured
|
|
140
|
+
*
|
|
141
|
+
* Note: SecurityChecker calls this method, so it cannot delegate back to SecurityChecker.
|
|
138
142
|
*
|
|
139
143
|
* @returns {object|null} Current user object or null if not authenticated
|
|
140
144
|
*/
|
|
141
145
|
getCurrentUser() {
|
|
142
|
-
|
|
146
|
+
if (this._getCurrentUserCallback) {
|
|
147
|
+
return this._getCurrentUserCallback()
|
|
148
|
+
}
|
|
149
|
+
// Subclasses can override this method
|
|
150
|
+
return null
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
|