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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoleGranterAdapter - Interface for role → permissions mapping
|
|
3
|
+
*
|
|
4
|
+
* The framework provides StaticRoleGranterAdapter (from config object).
|
|
5
|
+
* Apps can implement custom adapters (entity-based, API-backed, etc.)
|
|
6
|
+
*
|
|
7
|
+
* This interface abstracts HOW roles and permissions are stored/retrieved,
|
|
8
|
+
* allowing apps to use:
|
|
9
|
+
* - Static config (simple apps, demos)
|
|
10
|
+
* - Database/entity storage (apps with role management UI)
|
|
11
|
+
* - External API (microservices architecture)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Static adapter (auto-created from config)
|
|
15
|
+
* security: {
|
|
16
|
+
* role_permissions: { ROLE_USER: ['entity:*:read'] }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // Custom adapter
|
|
20
|
+
* security: {
|
|
21
|
+
* roleGranter: new EntityRoleGranterAdapter({ entityName: 'roles' })
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export class RoleGranterAdapter {
|
|
25
|
+
/**
|
|
26
|
+
* Get permissions granted to a role
|
|
27
|
+
*
|
|
28
|
+
* @param {string} role - Role name (e.g., 'ROLE_USER')
|
|
29
|
+
* @returns {string[]} Array of permission strings (may include wildcards)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* adapter.getPermissions('ROLE_ADMIN')
|
|
33
|
+
* // ['entity:**', 'admin:**']
|
|
34
|
+
*/
|
|
35
|
+
getPermissions(role) {
|
|
36
|
+
throw new Error('RoleGranterAdapter.getPermissions() must be implemented')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all defined roles
|
|
41
|
+
*
|
|
42
|
+
* @returns {string[]} Array of role names
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* adapter.getRoles()
|
|
46
|
+
* // ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN']
|
|
47
|
+
*/
|
|
48
|
+
getRoles() {
|
|
49
|
+
throw new Error('RoleGranterAdapter.getRoles() must be implemented')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get role hierarchy map
|
|
54
|
+
*
|
|
55
|
+
* @returns {Object<string, string[]>} Role → inherited roles
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* adapter.getHierarchy()
|
|
59
|
+
* // { ROLE_ADMIN: ['ROLE_USER'], ROLE_SUPER_ADMIN: ['ROLE_ADMIN'] }
|
|
60
|
+
*/
|
|
61
|
+
getHierarchy() {
|
|
62
|
+
throw new Error('RoleGranterAdapter.getHierarchy() must be implemented')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the role for unauthenticated users
|
|
67
|
+
*
|
|
68
|
+
* Always returns 'ROLE_ANONYMOUS' (convention)
|
|
69
|
+
*
|
|
70
|
+
* @returns {string} Anonymous role name
|
|
71
|
+
*/
|
|
72
|
+
getAnonymousRole() {
|
|
73
|
+
return 'ROLE_ANONYMOUS'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get role metadata (label, description)
|
|
78
|
+
* Optional - for display purposes
|
|
79
|
+
*
|
|
80
|
+
* @param {string} role - Role name
|
|
81
|
+
* @returns {RoleMeta|null}
|
|
82
|
+
*/
|
|
83
|
+
getRoleMeta(role) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Install adapter (called by Kernel when context is ready)
|
|
89
|
+
* Override for adapters that need initialization
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} ctx - Kernel context
|
|
92
|
+
*/
|
|
93
|
+
install(ctx) {
|
|
94
|
+
// Override if needed
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Uninstall adapter (cleanup)
|
|
99
|
+
*/
|
|
100
|
+
uninstall() {
|
|
101
|
+
// Override if needed
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if adapter supports persistence (editing + saving)
|
|
106
|
+
*
|
|
107
|
+
* When true, the adapter supports mutations (setRolePermissions, etc.)
|
|
108
|
+
* AND can persist changes. UI should show edit controls.
|
|
109
|
+
*
|
|
110
|
+
* When false, the adapter is read-only. UI should hide edit controls.
|
|
111
|
+
*
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
get canPersist() {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {Object} RoleMeta
|
|
121
|
+
* @property {string} [label] - Human-readable label
|
|
122
|
+
* @property {string} [description] - Description
|
|
123
|
+
*/
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoleGranterStorage - Storage bridge for roleGranter
|
|
3
|
+
*
|
|
4
|
+
* Implements Storage interface and delegates to roleGranter.
|
|
5
|
+
* Allows RolesManager to use standard EntityManager patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RoleGranterStorage {
|
|
9
|
+
/**
|
|
10
|
+
* Static capabilities (standard storage pattern)
|
|
11
|
+
* @type {import('../entity/storage/index.js').StorageCapabilities}
|
|
12
|
+
*/
|
|
13
|
+
static capabilities = {
|
|
14
|
+
supportsTotal: true,
|
|
15
|
+
supportsFilters: false,
|
|
16
|
+
supportsPagination: false,
|
|
17
|
+
supportsCaching: false // In-memory via roleGranter, no need for EntityManager cache
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('./RoleGranterAdapter.js').RoleGranterAdapter} roleGranter
|
|
22
|
+
*/
|
|
23
|
+
constructor(roleGranter) {
|
|
24
|
+
this._roleGranter = roleGranter
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Instance capabilities (merge static + dynamic)
|
|
29
|
+
*
|
|
30
|
+
* Dynamic properties:
|
|
31
|
+
* - requiresAuth: false (roles are system data)
|
|
32
|
+
* - readOnly: based on roleGranter.canPersist
|
|
33
|
+
*/
|
|
34
|
+
get capabilities() {
|
|
35
|
+
return {
|
|
36
|
+
...RoleGranterStorage.capabilities,
|
|
37
|
+
requiresAuth: false,
|
|
38
|
+
readOnly: !this._roleGranter?.canPersist
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List all roles
|
|
44
|
+
*/
|
|
45
|
+
async list(params = {}) {
|
|
46
|
+
if (!this._roleGranter) {
|
|
47
|
+
return { data: [], total: 0 }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ensure loaded for adapters that need it
|
|
51
|
+
if (this._roleGranter.ensureReady) {
|
|
52
|
+
await this._roleGranter.ensureReady()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const roleNames = this._roleGranter.getRoles()
|
|
56
|
+
const roles = roleNames.map(name => this._getRole(name))
|
|
57
|
+
|
|
58
|
+
// Simple search filter
|
|
59
|
+
let filtered = roles
|
|
60
|
+
if (params.search) {
|
|
61
|
+
const search = params.search.toLowerCase()
|
|
62
|
+
filtered = roles.filter(r =>
|
|
63
|
+
r.name.toLowerCase().includes(search) ||
|
|
64
|
+
r.label?.toLowerCase().includes(search)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
items: filtered,
|
|
70
|
+
total: filtered.length
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a single role by name
|
|
76
|
+
*/
|
|
77
|
+
async get(name) {
|
|
78
|
+
if (!this._roleGranter) return null
|
|
79
|
+
|
|
80
|
+
if (this._roleGranter.ensureReady) {
|
|
81
|
+
await this._roleGranter.ensureReady()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this._getRole(name)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get many roles by names
|
|
89
|
+
*/
|
|
90
|
+
async getMany(names) {
|
|
91
|
+
if (!this._roleGranter) return []
|
|
92
|
+
|
|
93
|
+
if (this._roleGranter.ensureReady) {
|
|
94
|
+
await this._roleGranter.ensureReady()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return names.map(name => this._getRole(name)).filter(Boolean)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a new role
|
|
102
|
+
*/
|
|
103
|
+
async create(data) {
|
|
104
|
+
if (!this._roleGranter?.createRole) {
|
|
105
|
+
throw new Error('Role creation not supported by this adapter')
|
|
106
|
+
}
|
|
107
|
+
return this._roleGranter.createRole(data.name, {
|
|
108
|
+
label: data.label,
|
|
109
|
+
permissions: data.permissions || [],
|
|
110
|
+
inherits: data.inherits || []
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Update an existing role
|
|
116
|
+
*/
|
|
117
|
+
async update(name, data) {
|
|
118
|
+
if (!this._roleGranter?.updateRole) {
|
|
119
|
+
throw new Error('Role update not supported by this adapter')
|
|
120
|
+
}
|
|
121
|
+
return this._roleGranter.updateRole(name, {
|
|
122
|
+
label: data.label,
|
|
123
|
+
permissions: data.permissions,
|
|
124
|
+
inherits: data.inherits
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Patch a role (partial update)
|
|
130
|
+
*/
|
|
131
|
+
async patch(name, data) {
|
|
132
|
+
return this.update(name, data)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete a role
|
|
137
|
+
*/
|
|
138
|
+
async delete(name) {
|
|
139
|
+
if (!this._roleGranter?.deleteRole) {
|
|
140
|
+
throw new Error('Role deletion not supported by this adapter')
|
|
141
|
+
}
|
|
142
|
+
return this._roleGranter.deleteRole(name)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get role object from roleGranter
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
_getRole(name) {
|
|
150
|
+
if (this._roleGranter.getRole) {
|
|
151
|
+
return this._roleGranter.getRole(name)
|
|
152
|
+
}
|
|
153
|
+
// Fallback for adapters without getRole
|
|
154
|
+
return {
|
|
155
|
+
name,
|
|
156
|
+
label: this._roleGranter.getLabels?.()[name] || name,
|
|
157
|
+
permissions: this._roleGranter.getPermissions(name),
|
|
158
|
+
inherits: this._roleGranter.getHierarchy()[name] || []
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolesManager - System entity manager for roles
|
|
3
|
+
*
|
|
4
|
+
* Uses RoleGranterStorage as bridge to roleGranter.
|
|
5
|
+
* Allows SecurityModule to use standard ctx.crud() pattern.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EntityManager } from '../entity/EntityManager.js'
|
|
9
|
+
import { RoleGranterStorage } from './RoleGranterStorage.js'
|
|
10
|
+
|
|
11
|
+
export class RolesManager extends EntityManager {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {import('./RoleGranterAdapter.js').RoleGranterAdapter} options.roleGranter
|
|
15
|
+
* @param {import('./PermissionRegistry.js').PermissionRegistry} options.permissionRegistry
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
const storage = new RoleGranterStorage(options.roleGranter)
|
|
19
|
+
|
|
20
|
+
super({
|
|
21
|
+
name: 'roles',
|
|
22
|
+
labelField: 'label',
|
|
23
|
+
idField: 'name',
|
|
24
|
+
system: true, // System-provided entity
|
|
25
|
+
fields: {
|
|
26
|
+
name: { type: 'text', label: 'Role Name', required: true },
|
|
27
|
+
label: { type: 'text', label: 'Display Label' },
|
|
28
|
+
permissions: { type: 'array', label: 'Permissions', default: [] },
|
|
29
|
+
inherits: { type: 'array', label: 'Inherits From', default: [] }
|
|
30
|
+
},
|
|
31
|
+
storage
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
this._roleGranter = options.roleGranter
|
|
35
|
+
this._permissionRegistry = options.permissionRegistry
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get roleGranter (for RoleForm permission picker)
|
|
40
|
+
*/
|
|
41
|
+
get roleGranter() {
|
|
42
|
+
return this._roleGranter
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get permission registry (for RoleForm permission picker)
|
|
47
|
+
*/
|
|
48
|
+
get permissionRegistry() {
|
|
49
|
+
return this._permissionRegistry
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if roles can be edited
|
|
54
|
+
*/
|
|
55
|
+
get canPersist() {
|
|
56
|
+
return this._roleGranter?.canPersist ?? false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Protected system roles that cannot be deleted
|
|
61
|
+
*/
|
|
62
|
+
static PROTECTED_ROLES = ['ROLE_ANONYMOUS']
|
|
63
|
+
|
|
64
|
+
// Permission checks based on canPersist
|
|
65
|
+
canCreate() { return this.canPersist }
|
|
66
|
+
canUpdate() { return this.canPersist }
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if can delete (general or row-specific)
|
|
70
|
+
* @param {object} [item] - Optional role to check
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
canDelete(item) {
|
|
74
|
+
if (!this.canPersist) return false
|
|
75
|
+
if (item) {
|
|
76
|
+
// Protected system roles cannot be deleted
|
|
77
|
+
return !RolesManager.PROTECTED_ROLES.includes(item.name)
|
|
78
|
+
}
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecurityModule - Role management UI
|
|
3
|
+
*
|
|
4
|
+
* Provides a page to view/edit roles and permissions.
|
|
5
|
+
* Works with any RoleGranterAdapter:
|
|
6
|
+
* - StaticRoleGranterAdapter: read-only view
|
|
7
|
+
* - PersistableRoleGranterAdapter: full CRUD
|
|
8
|
+
* - EntityRoleGranterAdapter: full CRUD via entity
|
|
9
|
+
*
|
|
10
|
+
* Registers permissions:
|
|
11
|
+
* - security:roles:read - View roles list
|
|
12
|
+
* - security:roles:create - Create new roles
|
|
13
|
+
* - security:roles:update - Edit existing roles
|
|
14
|
+
* - security:roles:delete - Delete roles
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { SecurityModule } from 'qdadm/security'
|
|
18
|
+
*
|
|
19
|
+
* const kernel = new Kernel({
|
|
20
|
+
* moduleDefs: [SecurityModule, ...],
|
|
21
|
+
* security: {
|
|
22
|
+
* roleGranter: createLocalStorageRoleGranter({ ... })
|
|
23
|
+
* }
|
|
24
|
+
* })
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { Module } from '../kernel/Module.js'
|
|
28
|
+
import { RolesManager } from './RolesManager.js'
|
|
29
|
+
|
|
30
|
+
export class SecurityModule extends Module {
|
|
31
|
+
static name = 'security'
|
|
32
|
+
static requires = []
|
|
33
|
+
static priority = 100 // Load late (after other modules register permissions)
|
|
34
|
+
|
|
35
|
+
async connect(ctx) {
|
|
36
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// PERMISSIONS
|
|
38
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
39
|
+
ctx.permissions('security', {
|
|
40
|
+
'roles:read': 'View roles and permissions',
|
|
41
|
+
'roles:create': 'Create new roles',
|
|
42
|
+
'roles:update': 'Edit role permissions',
|
|
43
|
+
'roles:delete': 'Delete roles'
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
47
|
+
// ENTITY (wraps roleGranter)
|
|
48
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
49
|
+
const rolesManager = new RolesManager({
|
|
50
|
+
roleGranter: ctx.security?.roleGranter,
|
|
51
|
+
permissionRegistry: ctx.permissionRegistry
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
ctx.entity('roles', rolesManager)
|
|
55
|
+
|
|
56
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
57
|
+
// ROUTES (using ctx.crud helper)
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
59
|
+
ctx.crud('roles', {
|
|
60
|
+
list: () => import('./pages/RoleList.vue'),
|
|
61
|
+
form: () => import('./pages/RoleForm.vue')
|
|
62
|
+
}, {
|
|
63
|
+
basePath: 'security/roles',
|
|
64
|
+
nav: {
|
|
65
|
+
section: 'Security',
|
|
66
|
+
icon: 'pi pi-shield',
|
|
67
|
+
permission: 'security:roles:read'
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default SecurityModule
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { RoleGranterAdapter } from './RoleGranterAdapter.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StaticRoleGranterAdapter - Role granter from config object
|
|
5
|
+
*
|
|
6
|
+
* Default implementation for simple apps and demos.
|
|
7
|
+
* Auto-created when passing role_permissions object to Kernel.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Auto-created by Kernel
|
|
11
|
+
* const kernel = new Kernel({
|
|
12
|
+
* security: {
|
|
13
|
+
* role_hierarchy: { ROLE_ADMIN: ['ROLE_USER'] },
|
|
14
|
+
* role_permissions: {
|
|
15
|
+
* ROLE_USER: ['entity:*:read'],
|
|
16
|
+
* ROLE_ADMIN: ['**']
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* // Or explicit creation
|
|
22
|
+
* const granter = new StaticRoleGranterAdapter({
|
|
23
|
+
* role_hierarchy: { ROLE_ADMIN: ['ROLE_USER'] },
|
|
24
|
+
* role_permissions: {
|
|
25
|
+
* ROLE_USER: ['entity:*:read'],
|
|
26
|
+
* ROLE_ADMIN: ['**']
|
|
27
|
+
* },
|
|
28
|
+
* role_labels: {
|
|
29
|
+
* ROLE_USER: 'User',
|
|
30
|
+
* ROLE_ADMIN: 'Administrator'
|
|
31
|
+
* }
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
export class StaticRoleGranterAdapter extends RoleGranterAdapter {
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} config
|
|
37
|
+
* @param {Object<string, string[]>} [config.role_hierarchy={}] - Role hierarchy
|
|
38
|
+
* @param {Object<string, string[]>} [config.role_permissions={}] - Role permissions
|
|
39
|
+
* @param {Object<string, string>} [config.role_labels={}] - Role display labels
|
|
40
|
+
*/
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
super()
|
|
43
|
+
this._hierarchy = config.role_hierarchy || {}
|
|
44
|
+
this._permissions = config.role_permissions || {}
|
|
45
|
+
this._labels = config.role_labels || {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get permissions for a role
|
|
50
|
+
* @param {string} role
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
getPermissions(role) {
|
|
54
|
+
return this._permissions[role] || []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all defined roles
|
|
59
|
+
* @returns {string[]}
|
|
60
|
+
*/
|
|
61
|
+
getRoles() {
|
|
62
|
+
// Combine roles from permissions and hierarchy
|
|
63
|
+
const roles = new Set([
|
|
64
|
+
...Object.keys(this._permissions),
|
|
65
|
+
...Object.keys(this._hierarchy)
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
// Also include inherited roles
|
|
69
|
+
for (const inherits of Object.values(this._hierarchy)) {
|
|
70
|
+
for (const r of inherits) {
|
|
71
|
+
roles.add(r)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [...roles]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get role hierarchy
|
|
80
|
+
* @returns {Object<string, string[]>}
|
|
81
|
+
*/
|
|
82
|
+
getHierarchy() {
|
|
83
|
+
return this._hierarchy
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get role metadata
|
|
88
|
+
* @param {string} role
|
|
89
|
+
* @returns {RoleMeta|null}
|
|
90
|
+
*/
|
|
91
|
+
getRoleMeta(role) {
|
|
92
|
+
const label = this._labels[role]
|
|
93
|
+
if (!label) return null
|
|
94
|
+
return { label }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Update role permissions at runtime (for testing or hot-reload)
|
|
99
|
+
* @param {string} role
|
|
100
|
+
* @param {string[]} permissions
|
|
101
|
+
*/
|
|
102
|
+
setPermissions(role, permissions) {
|
|
103
|
+
this._permissions[role] = permissions
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update role hierarchy at runtime
|
|
108
|
+
* @param {string} role
|
|
109
|
+
* @param {string[]} inherits
|
|
110
|
+
*/
|
|
111
|
+
setHierarchy(role, inherits) {
|
|
112
|
+
this._hierarchy[role] = inherits
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsersManager - System entity manager for users
|
|
3
|
+
*
|
|
4
|
+
* Provides standard user entity with:
|
|
5
|
+
* - username, password, role fields
|
|
6
|
+
* - role linked to roles entity via reference
|
|
7
|
+
* - Admin-only access by default (customizable)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // In a module:
|
|
11
|
+
* ctx.userEntity({
|
|
12
|
+
* storage: new ApiStorage({ endpoint: '/api/users' }),
|
|
13
|
+
* extraFields: {
|
|
14
|
+
* email: { type: 'email', label: 'Email' }
|
|
15
|
+
* }
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { EntityManager } from '../entity/EntityManager.js'
|
|
20
|
+
|
|
21
|
+
export class UsersManager extends EntityManager {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {Object} options.storage - Storage adapter (required)
|
|
25
|
+
* @param {Object} [options.extraFields={}] - Additional fields beyond username/password/role
|
|
26
|
+
* @param {string} [options.adminRole='ROLE_ADMIN'] - Role required for user management
|
|
27
|
+
* @param {boolean} [options.adminOnly=true] - Restrict access to admin role only
|
|
28
|
+
* @param {Object} [options.fieldOverrides={}] - Override default field configs
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
storage,
|
|
33
|
+
extraFields = {},
|
|
34
|
+
adminRole = 'ROLE_ADMIN',
|
|
35
|
+
adminOnly = true,
|
|
36
|
+
fieldOverrides = {},
|
|
37
|
+
...rest
|
|
38
|
+
} = options
|
|
39
|
+
|
|
40
|
+
if (!storage) {
|
|
41
|
+
throw new Error('[UsersManager] storage is required')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Default fields for users
|
|
45
|
+
const defaultFields = {
|
|
46
|
+
username: {
|
|
47
|
+
type: 'text',
|
|
48
|
+
label: 'Username',
|
|
49
|
+
required: true,
|
|
50
|
+
default: '',
|
|
51
|
+
...fieldOverrides.username
|
|
52
|
+
},
|
|
53
|
+
password: {
|
|
54
|
+
type: 'password',
|
|
55
|
+
label: 'Password',
|
|
56
|
+
required: true,
|
|
57
|
+
default: '',
|
|
58
|
+
listable: false, // Don't show in list view
|
|
59
|
+
...fieldOverrides.password
|
|
60
|
+
},
|
|
61
|
+
role: {
|
|
62
|
+
type: 'select',
|
|
63
|
+
label: 'Role',
|
|
64
|
+
reference: { entity: 'roles' }, // Links to roles entity
|
|
65
|
+
default: 'ROLE_USER',
|
|
66
|
+
...fieldOverrides.role
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
super({
|
|
71
|
+
name: 'users',
|
|
72
|
+
labelField: 'username',
|
|
73
|
+
system: true, // System-provided entity
|
|
74
|
+
fields: {
|
|
75
|
+
...defaultFields,
|
|
76
|
+
...extraFields
|
|
77
|
+
},
|
|
78
|
+
storage,
|
|
79
|
+
...rest
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
this._adminRole = adminRole
|
|
83
|
+
this._adminOnly = adminOnly
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if current user has admin role
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
_isAdmin() {
|
|
92
|
+
const user = this.authAdapter?.getCurrentUser?.()
|
|
93
|
+
if (!user) return false
|
|
94
|
+
|
|
95
|
+
// Check role directly or in roles array
|
|
96
|
+
const userRole = user.role || user.roles?.[0]
|
|
97
|
+
if (!userRole) return false
|
|
98
|
+
|
|
99
|
+
// Normalize to uppercase with ROLE_ prefix
|
|
100
|
+
const normalized = userRole.toUpperCase()
|
|
101
|
+
const roleToCheck = normalized.startsWith('ROLE_') ? normalized : `ROLE_${normalized}`
|
|
102
|
+
|
|
103
|
+
return roleToCheck === this._adminRole
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Permission checks - admin only by default
|
|
107
|
+
canRead() {
|
|
108
|
+
return this._adminOnly ? this._isAdmin() : true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
canCreate() {
|
|
112
|
+
return this._adminOnly ? this._isAdmin() : true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
canUpdate() {
|
|
116
|
+
return this._adminOnly ? this._isAdmin() : true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
canDelete() {
|
|
120
|
+
return this._adminOnly ? this._isAdmin() : true
|
|
121
|
+
}
|
|
122
|
+
}
|