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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { RoleGranterAdapter } from './RoleGranterAdapter.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EntityRoleGranterAdapter - Role granter backed by an entity
|
|
5
|
+
*
|
|
6
|
+
* Fetches roles and permissions from a 'roles' entity.
|
|
7
|
+
* Auto-invalidates cache on entity:roles:* signals.
|
|
8
|
+
* Use this for apps with a role management UI.
|
|
9
|
+
*
|
|
10
|
+
* Expected entity schema:
|
|
11
|
+
* {
|
|
12
|
+
* id: 'role-1',
|
|
13
|
+
* name: 'ROLE_ADMIN',
|
|
14
|
+
* label: 'Administrator',
|
|
15
|
+
* permissions: ['entity:**', 'admin:**'],
|
|
16
|
+
* inherits: ['ROLE_USER']
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Configure Kernel with entity-based roles
|
|
21
|
+
* const kernel = new Kernel({
|
|
22
|
+
* modules: [SecurityModule, ...],
|
|
23
|
+
* security: {
|
|
24
|
+
* roleGranter: new EntityRoleGranterAdapter({
|
|
25
|
+
* entityName: 'roles',
|
|
26
|
+
* nameField: 'name',
|
|
27
|
+
* permissionsField: 'permissions',
|
|
28
|
+
* inheritsField: 'inherits'
|
|
29
|
+
* })
|
|
30
|
+
* }
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* // Load roles before boot
|
|
34
|
+
* await kernel.options.security.roleGranter.load()
|
|
35
|
+
* await kernel.boot()
|
|
36
|
+
*/
|
|
37
|
+
export class EntityRoleGranterAdapter extends RoleGranterAdapter {
|
|
38
|
+
/**
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
* @param {string} [options.entityName='roles'] - Entity name for roles
|
|
41
|
+
* @param {string} [options.nameField='name'] - Field for role name
|
|
42
|
+
* @param {string} [options.labelField='label'] - Field for display label
|
|
43
|
+
* @param {string} [options.permissionsField='permissions'] - Field for permissions array
|
|
44
|
+
* @param {string} [options.inheritsField='inherits'] - Field for parent roles (hierarchy)
|
|
45
|
+
*/
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
super()
|
|
48
|
+
this._entityName = options.entityName || 'roles'
|
|
49
|
+
this._nameField = options.nameField || 'name'
|
|
50
|
+
this._labelField = options.labelField || 'label'
|
|
51
|
+
this._permissionsField = options.permissionsField || 'permissions'
|
|
52
|
+
this._inheritsField = options.inheritsField || 'inherits'
|
|
53
|
+
|
|
54
|
+
this._cache = null
|
|
55
|
+
this._ctx = null
|
|
56
|
+
this._orchestrator = null
|
|
57
|
+
this._signalCleanup = null
|
|
58
|
+
this._loading = null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Install adapter (called by Kernel after orchestrator ready)
|
|
63
|
+
* @param {Object} ctx - Kernel context
|
|
64
|
+
*/
|
|
65
|
+
install(ctx) {
|
|
66
|
+
this._ctx = ctx
|
|
67
|
+
this._orchestrator = ctx.orchestrator
|
|
68
|
+
|
|
69
|
+
// Auto-invalidate on role changes
|
|
70
|
+
if (ctx.signals) {
|
|
71
|
+
this._signalCleanup = ctx.signals.on(`entity:${this._entityName}:**`, () => {
|
|
72
|
+
this.invalidate()
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Uninstall adapter (cleanup)
|
|
79
|
+
*/
|
|
80
|
+
uninstall() {
|
|
81
|
+
if (this._signalCleanup) {
|
|
82
|
+
this._signalCleanup()
|
|
83
|
+
this._signalCleanup = null
|
|
84
|
+
}
|
|
85
|
+
this._ctx = null
|
|
86
|
+
this._orchestrator = null
|
|
87
|
+
this._cache = null
|
|
88
|
+
this._loading = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load roles from entity (async, must be called before use)
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async load() {
|
|
96
|
+
// Prevent concurrent loads
|
|
97
|
+
if (this._loading) {
|
|
98
|
+
return this._loading
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this._loading = this._doLoad()
|
|
102
|
+
await this._loading
|
|
103
|
+
this._loading = null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Internal load implementation
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
async _doLoad() {
|
|
111
|
+
const manager = this._orchestrator?.get(this._entityName)
|
|
112
|
+
if (!manager) {
|
|
113
|
+
console.warn(`[EntityRoleGranter] Entity '${this._entityName}' not found`)
|
|
114
|
+
this._cache = { permissions: {}, hierarchy: {}, labels: {} }
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { data: roles } = await manager.list({ limit: 1000 })
|
|
120
|
+
|
|
121
|
+
// Build cache
|
|
122
|
+
const permissions = {}
|
|
123
|
+
const hierarchy = {}
|
|
124
|
+
const labels = {}
|
|
125
|
+
|
|
126
|
+
for (const role of roles) {
|
|
127
|
+
const name = role[this._nameField]
|
|
128
|
+
if (!name) continue
|
|
129
|
+
|
|
130
|
+
permissions[name] = role[this._permissionsField] || []
|
|
131
|
+
labels[name] = role[this._labelField] || name
|
|
132
|
+
|
|
133
|
+
const inherits = role[this._inheritsField]
|
|
134
|
+
if (inherits?.length > 0) {
|
|
135
|
+
hierarchy[name] = inherits
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._cache = { permissions, hierarchy, labels }
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`[EntityRoleGranter] Failed to load roles:`, error)
|
|
142
|
+
this._cache = { permissions: {}, hierarchy: {}, labels: {} }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Ensure cache is loaded (throws if not)
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
_ensureLoaded() {
|
|
151
|
+
if (!this._cache) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
'[EntityRoleGranter] Roles not loaded. Call load() before using the adapter.'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get permissions for a role
|
|
160
|
+
* @param {string} role
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
getPermissions(role) {
|
|
164
|
+
this._ensureLoaded()
|
|
165
|
+
return this._cache.permissions[role] || []
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get all defined roles
|
|
170
|
+
* @returns {string[]}
|
|
171
|
+
*/
|
|
172
|
+
getRoles() {
|
|
173
|
+
this._ensureLoaded()
|
|
174
|
+
return Object.keys(this._cache.permissions)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get role hierarchy
|
|
179
|
+
* @returns {Object<string, string[]>}
|
|
180
|
+
*/
|
|
181
|
+
getHierarchy() {
|
|
182
|
+
this._ensureLoaded()
|
|
183
|
+
return this._cache.hierarchy
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get role metadata
|
|
188
|
+
* @param {string} role
|
|
189
|
+
* @returns {RoleMeta|null}
|
|
190
|
+
*/
|
|
191
|
+
getRoleMeta(role) {
|
|
192
|
+
this._ensureLoaded()
|
|
193
|
+
const label = this._cache.labels[role]
|
|
194
|
+
if (!label) return null
|
|
195
|
+
return { label }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Invalidate cache (triggers reload on next access)
|
|
200
|
+
* Call this after role changes
|
|
201
|
+
*/
|
|
202
|
+
invalidate() {
|
|
203
|
+
this._cache = null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if cache is loaded
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
get isLoaded() {
|
|
211
|
+
return this._cache !== null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Entity-backed adapter can always persist (via entity manager)
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
get canPersist() {
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
// Role query methods
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if a role exists
|
|
228
|
+
* @param {string} role - Role name
|
|
229
|
+
* @returns {boolean}
|
|
230
|
+
*/
|
|
231
|
+
roleExists(role) {
|
|
232
|
+
this._ensureLoaded()
|
|
233
|
+
return this._cache.permissions[role] !== undefined
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get complete role object
|
|
238
|
+
* @param {string} role - Role name
|
|
239
|
+
* @returns {Object|null}
|
|
240
|
+
*/
|
|
241
|
+
getRole(role) {
|
|
242
|
+
this._ensureLoaded()
|
|
243
|
+
if (!this.roleExists(role)) {
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
name: role,
|
|
248
|
+
label: this._cache.labels[role] || role,
|
|
249
|
+
permissions: this._cache.permissions[role] || [],
|
|
250
|
+
inherits: this._cache.hierarchy[role] || []
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
255
|
+
// Mutation methods (via entity manager)
|
|
256
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the entity manager for roles
|
|
260
|
+
* @returns {Object}
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
_getManager() {
|
|
264
|
+
const manager = this._orchestrator?.get(this._entityName)
|
|
265
|
+
if (!manager) {
|
|
266
|
+
throw new Error(`Entity '${this._entityName}' not found`)
|
|
267
|
+
}
|
|
268
|
+
return manager
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a new role
|
|
273
|
+
* @param {string} name - Role name
|
|
274
|
+
* @param {Object} [options]
|
|
275
|
+
* @param {string} [options.label] - Display label
|
|
276
|
+
* @param {string[]} [options.permissions=[]] - Permissions
|
|
277
|
+
* @param {string[]} [options.inherits=[]] - Parent roles
|
|
278
|
+
* @returns {Promise<Object>} Created role entity
|
|
279
|
+
*/
|
|
280
|
+
async createRole(name, { label, permissions = [], inherits = [] } = {}) {
|
|
281
|
+
if (this.roleExists(name)) {
|
|
282
|
+
throw new Error(`Role '${name}' already exists`)
|
|
283
|
+
}
|
|
284
|
+
const manager = this._getManager()
|
|
285
|
+
const data = {
|
|
286
|
+
[this._nameField]: name,
|
|
287
|
+
[this._labelField]: label || name,
|
|
288
|
+
[this._permissionsField]: permissions,
|
|
289
|
+
[this._inheritsField]: inherits
|
|
290
|
+
}
|
|
291
|
+
const result = await manager.create(data)
|
|
292
|
+
this.invalidate()
|
|
293
|
+
return result
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Update an existing role
|
|
298
|
+
* @param {string} name - Role name
|
|
299
|
+
* @param {Object} [options]
|
|
300
|
+
* @param {string} [options.label] - Display label
|
|
301
|
+
* @param {string[]} [options.permissions] - Permissions
|
|
302
|
+
* @param {string[]} [options.inherits] - Parent roles
|
|
303
|
+
* @returns {Promise<Object>} Updated role entity
|
|
304
|
+
*/
|
|
305
|
+
async updateRole(name, { label, permissions, inherits } = {}) {
|
|
306
|
+
this._ensureLoaded()
|
|
307
|
+
const manager = this._getManager()
|
|
308
|
+
|
|
309
|
+
// Find the role entity by name
|
|
310
|
+
const { data: roles } = await manager.list({
|
|
311
|
+
filter: { [this._nameField]: name },
|
|
312
|
+
limit: 1
|
|
313
|
+
})
|
|
314
|
+
if (roles.length === 0) {
|
|
315
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const roleEntity = roles[0]
|
|
319
|
+
const updates = {}
|
|
320
|
+
if (label !== undefined) updates[this._labelField] = label
|
|
321
|
+
if (permissions !== undefined) updates[this._permissionsField] = permissions
|
|
322
|
+
if (inherits !== undefined) updates[this._inheritsField] = inherits
|
|
323
|
+
|
|
324
|
+
const result = await manager.update(roleEntity.id, updates)
|
|
325
|
+
this.invalidate()
|
|
326
|
+
return result
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Delete a role
|
|
331
|
+
* @param {string} name - Role name
|
|
332
|
+
* @returns {Promise<void>}
|
|
333
|
+
*/
|
|
334
|
+
async deleteRole(name) {
|
|
335
|
+
this._ensureLoaded()
|
|
336
|
+
const manager = this._getManager()
|
|
337
|
+
|
|
338
|
+
// Find the role entity by name
|
|
339
|
+
const { data: roles } = await manager.list({
|
|
340
|
+
filter: { [this._nameField]: name },
|
|
341
|
+
limit: 1
|
|
342
|
+
})
|
|
343
|
+
if (roles.length === 0) {
|
|
344
|
+
throw new Error(`Role '${name}' does not exist`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
await manager.delete(roles[0].id)
|
|
348
|
+
this.invalidate()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionMatcher - Wildcard permission matching
|
|
3
|
+
*
|
|
4
|
+
* Supports two wildcard patterns (similar to signals):
|
|
5
|
+
* - `*` matches exactly one segment
|
|
6
|
+
* - `**` matches zero or more segments (greedy)
|
|
7
|
+
*
|
|
8
|
+
* Permission format: namespace:target:action
|
|
9
|
+
* Examples:
|
|
10
|
+
* entity:books:read - specific permission
|
|
11
|
+
* entity:books:* - any action on books
|
|
12
|
+
* entity:*:read - read on any entity
|
|
13
|
+
* entity:** - all entity permissions
|
|
14
|
+
* ** - super admin (matches everything)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
|
|
18
|
+
* PermissionMatcher.matches('entity:**', 'entity:books:read') // true
|
|
19
|
+
* PermissionMatcher.matches('**', 'anything:here') // true
|
|
20
|
+
*/
|
|
21
|
+
export class PermissionMatcher {
|
|
22
|
+
/**
|
|
23
|
+
* Check if a permission pattern matches a required permission
|
|
24
|
+
*
|
|
25
|
+
* @param {string} pattern - Pattern with wildcards (e.g., 'entity:*:read')
|
|
26
|
+
* @param {string} required - Required permission (e.g., 'entity:books:read')
|
|
27
|
+
* @returns {boolean} True if pattern matches required
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* PermissionMatcher.matches('entity:books:read', 'entity:books:read') // true (exact)
|
|
31
|
+
* PermissionMatcher.matches('entity:books:*', 'entity:books:read') // true (* = one segment)
|
|
32
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:books:read') // true
|
|
33
|
+
* PermissionMatcher.matches('entity:**', 'entity:books:read') // true (** = greedy)
|
|
34
|
+
* PermissionMatcher.matches('**', 'entity:books:read') // true (super admin)
|
|
35
|
+
* PermissionMatcher.matches('entity:*:read', 'entity:a:b:read') // false (* = exactly one)
|
|
36
|
+
*/
|
|
37
|
+
static matches(pattern, required) {
|
|
38
|
+
// Super admin
|
|
39
|
+
if (pattern === '**') return true
|
|
40
|
+
|
|
41
|
+
// Exact match
|
|
42
|
+
if (pattern === required) return true
|
|
43
|
+
|
|
44
|
+
const patternParts = pattern.split(':')
|
|
45
|
+
const requiredParts = required.split(':')
|
|
46
|
+
|
|
47
|
+
return this._matchParts(patternParts, requiredParts, 0, 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Recursive matching of pattern parts against required parts
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
static _matchParts(pattern, required, pi, ri) {
|
|
55
|
+
// Both exhausted = match
|
|
56
|
+
if (pi === pattern.length && ri === required.length) {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Pattern exhausted but required has more = no match
|
|
61
|
+
if (pi === pattern.length) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const p = pattern[pi]
|
|
66
|
+
|
|
67
|
+
// ** matches zero or more remaining segments
|
|
68
|
+
if (p === '**') {
|
|
69
|
+
// ** at end matches everything remaining
|
|
70
|
+
if (pi === pattern.length - 1) {
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try matching ** against 0, 1, 2, ... segments
|
|
75
|
+
for (let skip = 0; skip <= required.length - ri; skip++) {
|
|
76
|
+
if (this._matchParts(pattern, required, pi + 1, ri + skip)) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Required exhausted but pattern has more (and not **)
|
|
84
|
+
if (ri === required.length) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// * matches exactly one segment
|
|
89
|
+
if (p === '*') {
|
|
90
|
+
return this._matchParts(pattern, required, pi + 1, ri + 1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Literal match
|
|
94
|
+
if (p === required[ri]) {
|
|
95
|
+
return this._matchParts(pattern, required, pi + 1, ri + 1)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if any pattern in array matches the required permission
|
|
103
|
+
*
|
|
104
|
+
* @param {string[]} patterns - Array of patterns (user's permissions)
|
|
105
|
+
* @param {string} required - Required permission to check
|
|
106
|
+
* @returns {boolean} True if any pattern matches
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const userPerms = ['entity:*:read', 'entity:*:list', 'auth:impersonate']
|
|
110
|
+
* PermissionMatcher.any(userPerms, 'entity:books:read') // true
|
|
111
|
+
* PermissionMatcher.any(userPerms, 'entity:books:delete') // false
|
|
112
|
+
*/
|
|
113
|
+
static any(patterns, required) {
|
|
114
|
+
if (!patterns || patterns.length === 0) return false
|
|
115
|
+
return patterns.some(p => this.matches(p, required))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filter permissions that match a pattern
|
|
120
|
+
*
|
|
121
|
+
* @param {string[]} permissions - Array of specific permissions
|
|
122
|
+
* @param {string} pattern - Pattern to filter by
|
|
123
|
+
* @returns {string[]} Permissions that match the pattern
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* const allPerms = ['entity:books:read', 'entity:books:create', 'auth:login']
|
|
127
|
+
* PermissionMatcher.filter(allPerms, 'entity:books:*') // ['entity:books:read', 'entity:books:create']
|
|
128
|
+
*/
|
|
129
|
+
static filter(permissions, pattern) {
|
|
130
|
+
return permissions.filter(p => this.matches(pattern, p))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Expand a pattern against registered permissions
|
|
135
|
+
* Useful for UI: show what a wildcard pattern actually grants
|
|
136
|
+
*
|
|
137
|
+
* @param {string} pattern - Pattern to expand
|
|
138
|
+
* @param {string[]} allPermissions - All registered permissions
|
|
139
|
+
* @returns {string[]} Permissions that the pattern would grant
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const registry = ['entity:books:read', 'entity:books:create', 'entity:loans:read']
|
|
143
|
+
* PermissionMatcher.expand('entity:*:read', registry) // ['entity:books:read', 'entity:loans:read']
|
|
144
|
+
*/
|
|
145
|
+
static expand(pattern, allPermissions) {
|
|
146
|
+
return allPermissions.filter(p => this.matches(pattern, p))
|
|
147
|
+
}
|
|
148
|
+
}
|