@quazardous/qdadm 1.19.3
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/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- package/src/zones/zones.ts +191 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role hierarchy configuration type
|
|
3
|
+
*/
|
|
4
|
+
export type RoleHierarchyConfig = Record<string, string[]>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RoleHierarchy - Topological role resolution for Symfony-like permission system
|
|
8
|
+
*
|
|
9
|
+
* Roles form a Directed Acyclic Graph (DAG) where higher roles inherit
|
|
10
|
+
* permissions from lower roles. This class resolves the complete set of
|
|
11
|
+
* roles that a user effectively has based on their assigned roles.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const hierarchy = new RoleHierarchy({
|
|
16
|
+
* ROLE_ADMIN: ['ROLE_USER'], // Admin inherits from User
|
|
17
|
+
* ROLE_SUPER_ADMIN: ['ROLE_ADMIN'], // Super admin inherits from Admin
|
|
18
|
+
* ROLE_MANAGER: ['ROLE_USER'], // Manager also inherits from User
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* hierarchy.getReachableRoles('ROLE_ADMIN')
|
|
22
|
+
* // Returns: ['ROLE_ADMIN', 'ROLE_USER']
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class RoleHierarchy {
|
|
26
|
+
readonly map: RoleHierarchyConfig
|
|
27
|
+
|
|
28
|
+
constructor(hierarchy: RoleHierarchyConfig = {}) {
|
|
29
|
+
this.map = hierarchy
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve all roles reachable from a given role (topological traversal)
|
|
34
|
+
*
|
|
35
|
+
* Performs BFS traversal of the role graph to find all inherited roles.
|
|
36
|
+
* Handles cycles gracefully by tracking visited nodes.
|
|
37
|
+
*/
|
|
38
|
+
getReachableRoles(role: string): string[] {
|
|
39
|
+
const visited = new Set<string>()
|
|
40
|
+
const queue: string[] = [role]
|
|
41
|
+
|
|
42
|
+
while (queue.length > 0) {
|
|
43
|
+
const current = queue.shift()!
|
|
44
|
+
if (visited.has(current)) continue
|
|
45
|
+
visited.add(current)
|
|
46
|
+
|
|
47
|
+
const parents = this.map[current] || []
|
|
48
|
+
queue.push(...parents)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [...visited]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if user with given roles has the required role
|
|
56
|
+
*
|
|
57
|
+
* A user has a role if:
|
|
58
|
+
* 1. They are directly assigned that role, OR
|
|
59
|
+
* 2. They have a role that inherits from the required role
|
|
60
|
+
*/
|
|
61
|
+
isGrantedRole(userRoles: string | string[], requiredRole: string): boolean {
|
|
62
|
+
const roles = Array.isArray(userRoles) ? userRoles : [userRoles]
|
|
63
|
+
|
|
64
|
+
for (const role of roles) {
|
|
65
|
+
const reachable = this.getReachableRoles(role)
|
|
66
|
+
if (reachable.includes(requiredRole)) return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all roles that can reach a given role (reverse lookup)
|
|
74
|
+
*
|
|
75
|
+
* Useful for finding which roles would grant a specific permission.
|
|
76
|
+
*/
|
|
77
|
+
getRolesGranting(targetRole: string): string[] {
|
|
78
|
+
const grantors: string[] = []
|
|
79
|
+
|
|
80
|
+
for (const role of Object.keys(this.map)) {
|
|
81
|
+
if (this.isGrantedRole([role], targetRole)) {
|
|
82
|
+
grantors.push(role)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Also check the target role itself
|
|
87
|
+
if (!grantors.includes(targetRole)) {
|
|
88
|
+
grantors.push(targetRole)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return grantors
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate the hierarchy for cycles (optional sanity check)
|
|
96
|
+
*/
|
|
97
|
+
validate(): boolean {
|
|
98
|
+
const visiting = new Set<string>()
|
|
99
|
+
const visited = new Set<string>()
|
|
100
|
+
|
|
101
|
+
const hasCycle = (role: string): boolean => {
|
|
102
|
+
if (visited.has(role)) return false
|
|
103
|
+
if (visiting.has(role)) return true
|
|
104
|
+
|
|
105
|
+
visiting.add(role)
|
|
106
|
+
const parents = this.map[role] || []
|
|
107
|
+
for (const parent of parents) {
|
|
108
|
+
if (hasCycle(parent)) return true
|
|
109
|
+
}
|
|
110
|
+
visiting.delete(role)
|
|
111
|
+
visited.add(role)
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const role of Object.keys(this.map)) {
|
|
116
|
+
if (hasCycle(role)) return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a RoleHierarchy instance from config
|
|
125
|
+
*/
|
|
126
|
+
export function createRoleHierarchy(config: RoleHierarchyConfig = {}): RoleHierarchy {
|
|
127
|
+
return new RoleHierarchy(config)
|
|
128
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { RoleHierarchy, type RoleHierarchyConfig } from './RoleHierarchy'
|
|
2
|
+
import { PermissionMatcher } from '../../security/PermissionMatcher'
|
|
3
|
+
import { StaticRoleProvider } from '../../security/StaticRolesProvider'
|
|
4
|
+
import type { AuthUser } from './EntityAuthAdapter'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* RolesProvider adapter interface (from security module)
|
|
8
|
+
*/
|
|
9
|
+
export interface RoleProvider {
|
|
10
|
+
getPermissions(role: string): string[]
|
|
11
|
+
getRoles(): string[]
|
|
12
|
+
getHierarchy(): RoleHierarchyConfig
|
|
13
|
+
getAnonymousRole(): string
|
|
14
|
+
getRoleMeta(role: string): { label?: string; description?: string } | null
|
|
15
|
+
readonly canPersist: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SecurityChecker constructor options
|
|
20
|
+
*/
|
|
21
|
+
export interface SecurityCheckerOptions {
|
|
22
|
+
rolesProvider?: RoleProvider
|
|
23
|
+
roleHierarchy?: RoleHierarchy | RoleHierarchyConfig
|
|
24
|
+
rolePermissions?: Record<string, string[]>
|
|
25
|
+
getCurrentUser: () => AuthUser | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* SecurityChecker config for factory
|
|
30
|
+
*/
|
|
31
|
+
export interface SecurityCheckerConfig {
|
|
32
|
+
rolesProvider?: RoleProvider
|
|
33
|
+
role_hierarchy?: RoleHierarchyConfig
|
|
34
|
+
role_permissions?: Record<string, string[]>
|
|
35
|
+
getCurrentUser: () => AuthUser | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SecurityChecker - Symfony-inspired permission checking
|
|
40
|
+
*
|
|
41
|
+
* Provides the `isGranted(attribute, subject?)` contract for checking permissions.
|
|
42
|
+
* Supports both role checks (ROLE_*) and permission checks with wildcards.
|
|
43
|
+
*
|
|
44
|
+
* Permission format: namespace:target:action
|
|
45
|
+
* - entity:books:read - Entity CRUD
|
|
46
|
+
* - auth:impersonate - System feature
|
|
47
|
+
* - admin:config:edit - Admin feature
|
|
48
|
+
*
|
|
49
|
+
* Wildcard patterns (like signals):
|
|
50
|
+
* - `*` matches exactly one segment
|
|
51
|
+
* - `**` matches zero or more segments (greedy)
|
|
52
|
+
*/
|
|
53
|
+
export class SecurityChecker {
|
|
54
|
+
protected _rolesProvider: RoleProvider
|
|
55
|
+
protected _legacyRoleHierarchy: RoleHierarchy | null
|
|
56
|
+
readonly getCurrentUser: () => AuthUser | null
|
|
57
|
+
|
|
58
|
+
constructor({ rolesProvider, roleHierarchy, rolePermissions, getCurrentUser }: SecurityCheckerOptions) {
|
|
59
|
+
// Support both new rolesProvider and legacy rolePermissions
|
|
60
|
+
if (rolesProvider) {
|
|
61
|
+
this._rolesProvider = rolesProvider
|
|
62
|
+
this._legacyRoleHierarchy = null
|
|
63
|
+
} else {
|
|
64
|
+
// Legacy: create static provider from rolePermissions
|
|
65
|
+
const hierarchyConfig =
|
|
66
|
+
roleHierarchy instanceof RoleHierarchy ? {} : (roleHierarchy as RoleHierarchyConfig) || {}
|
|
67
|
+
|
|
68
|
+
this._rolesProvider = new StaticRoleProvider({
|
|
69
|
+
role_hierarchy: hierarchyConfig,
|
|
70
|
+
role_permissions: rolePermissions || {},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
this._legacyRoleHierarchy =
|
|
74
|
+
roleHierarchy instanceof RoleHierarchy
|
|
75
|
+
? roleHierarchy
|
|
76
|
+
: new RoleHierarchy(hierarchyConfig)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.getCurrentUser = getCurrentUser
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get role hierarchy (dynamically resolved from rolesProvider)
|
|
84
|
+
*/
|
|
85
|
+
get roleHierarchy(): RoleHierarchy {
|
|
86
|
+
if (this._legacyRoleHierarchy) {
|
|
87
|
+
return this._legacyRoleHierarchy
|
|
88
|
+
}
|
|
89
|
+
return new RoleHierarchy(this._rolesProvider.getHierarchy())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get roles provider adapter
|
|
94
|
+
*/
|
|
95
|
+
get rolesProvider(): RoleProvider {
|
|
96
|
+
return this._rolesProvider
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get role permissions (for backward compatibility / debug panel)
|
|
101
|
+
*/
|
|
102
|
+
get rolePermissions(): Record<string, string[]> {
|
|
103
|
+
const roles = this._rolesProvider.getRoles()
|
|
104
|
+
const perms: Record<string, string[]> = {}
|
|
105
|
+
for (const role of roles) {
|
|
106
|
+
perms[role] = this._rolesProvider.getPermissions(role)
|
|
107
|
+
}
|
|
108
|
+
return perms
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if current user is granted an attribute (role or permission)
|
|
113
|
+
*
|
|
114
|
+
* This is the main contract method, similar to Symfony's isGranted().
|
|
115
|
+
*/
|
|
116
|
+
isGranted(attribute: string, _subject: unknown = null): boolean {
|
|
117
|
+
const user = this.getCurrentUser()
|
|
118
|
+
if (!user) return false
|
|
119
|
+
|
|
120
|
+
// 1. Check if it's a role (ROLE_*)
|
|
121
|
+
if (attribute.startsWith('ROLE_')) {
|
|
122
|
+
return this.roleHierarchy.isGrantedRole(user.roles || [user.role || ''], attribute)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Check if it's a permission (with wildcard support)
|
|
126
|
+
const userPerms = this.getUserPermissions(user)
|
|
127
|
+
return PermissionMatcher.any(userPerms, attribute)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get all permissions for a user (resolved from roles + user overrides)
|
|
132
|
+
*/
|
|
133
|
+
getUserPermissions(user: AuthUser): string[] {
|
|
134
|
+
const roles = user.roles || [user.role || '']
|
|
135
|
+
const perms = new Set<string>()
|
|
136
|
+
|
|
137
|
+
for (const role of roles) {
|
|
138
|
+
if (!role) continue
|
|
139
|
+
const reachable = this.roleHierarchy.getReachableRoles(role)
|
|
140
|
+
for (const r of reachable) {
|
|
141
|
+
const rolePerms = this._rolesProvider.getPermissions(r)
|
|
142
|
+
rolePerms.forEach((p) => perms.add(p))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// User-specific permission overrides
|
|
147
|
+
if (user.permissions && Array.isArray(user.permissions)) {
|
|
148
|
+
user.permissions.forEach((p) => perms.add(p))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [...perms]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if user can assign a role to another user
|
|
156
|
+
*/
|
|
157
|
+
canAssignRole(targetRole: string): boolean {
|
|
158
|
+
return this.isGranted('security:roles:assign') && this.isGranted(targetRole)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get all roles that current user can assign
|
|
163
|
+
*/
|
|
164
|
+
getAssignableRoles(): string[] {
|
|
165
|
+
if (!this.isGranted('security:roles:assign')) return []
|
|
166
|
+
|
|
167
|
+
const user = this.getCurrentUser()
|
|
168
|
+
if (!user) return []
|
|
169
|
+
|
|
170
|
+
const userRoles = user.roles || [user.role || '']
|
|
171
|
+
const assignable = new Set<string>()
|
|
172
|
+
|
|
173
|
+
for (const role of userRoles) {
|
|
174
|
+
if (!role) continue
|
|
175
|
+
const reachable = this.roleHierarchy.getReachableRoles(role)
|
|
176
|
+
reachable.forEach((r) => assignable.add(r))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return [...assignable]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create a SecurityChecker instance from config
|
|
185
|
+
*/
|
|
186
|
+
export function createSecurityChecker(config: SecurityCheckerConfig): SecurityChecker {
|
|
187
|
+
if (config.rolesProvider) {
|
|
188
|
+
return new SecurityChecker({
|
|
189
|
+
rolesProvider: config.rolesProvider,
|
|
190
|
+
getCurrentUser: config.getCurrentUser,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Legacy config: auto-create static provider
|
|
195
|
+
return new SecurityChecker({
|
|
196
|
+
rolesProvider: new StaticRoleProvider({
|
|
197
|
+
role_hierarchy: config.role_hierarchy || {},
|
|
198
|
+
role_permissions: config.role_permissions || {},
|
|
199
|
+
}),
|
|
200
|
+
getCurrentUser: config.getCurrentUser,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Factory/Resolver Pattern
|
|
3
|
+
*
|
|
4
|
+
* Enables declarative auth adapter configuration with auto-resolution.
|
|
5
|
+
* Follows the same pattern as storage/factory.js for consistency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EntityAuthAdapter, type EntityAuthAdapterOptions, type AuthUser } from './EntityAuthAdapter'
|
|
9
|
+
import { PermissiveAuthAdapter } from './PermissiveAdapter'
|
|
10
|
+
import type { CompositeAuthAdapter, CompositeAuthConfig } from './CompositeAuthAdapter'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Auth adapter class type
|
|
14
|
+
*/
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
type AuthAdapterClass = new (options?: any) => EntityAuthAdapter
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Built-in auth adapter types
|
|
20
|
+
*/
|
|
21
|
+
export const authTypes: Record<string, AuthAdapterClass> = {
|
|
22
|
+
permissive: PermissiveAuthAdapter,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parsed auth pattern
|
|
27
|
+
*/
|
|
28
|
+
export interface ParsedAuthPattern {
|
|
29
|
+
type: string
|
|
30
|
+
scope?: string
|
|
31
|
+
[key: string]: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Auth config object
|
|
36
|
+
*/
|
|
37
|
+
export interface AuthConfig {
|
|
38
|
+
type?: string
|
|
39
|
+
[key: string]: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Auth factory context
|
|
44
|
+
*/
|
|
45
|
+
export interface AuthFactoryContext {
|
|
46
|
+
authTypes?: Record<string, AuthAdapterClass>
|
|
47
|
+
authResolver?: AuthResolver
|
|
48
|
+
CompositeAuthAdapter?: new (config: CompositeAuthConfig, context: AuthFactoryContext) => CompositeAuthAdapter
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Auth resolver function type
|
|
53
|
+
*/
|
|
54
|
+
export type AuthResolver = (config: AuthConfig, context: AuthFactoryContext) => EntityAuthAdapter
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse auth pattern 'type' or 'type:scope'
|
|
58
|
+
*/
|
|
59
|
+
export function parseAuthPattern(pattern: string): ParsedAuthPattern | null {
|
|
60
|
+
if (typeof pattern !== 'string') return null
|
|
61
|
+
|
|
62
|
+
const match = pattern.match(/^(\w+)(?::(.+))?$/)
|
|
63
|
+
if (match && match[1]) {
|
|
64
|
+
const type = match[1]
|
|
65
|
+
const scope = match[2]
|
|
66
|
+
return scope ? { type, scope } : { type }
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Default auth resolver - creates adapter instance from config
|
|
73
|
+
*/
|
|
74
|
+
export function defaultAuthResolver(config: AuthConfig, context: AuthFactoryContext = {}): EntityAuthAdapter {
|
|
75
|
+
const { type, ...rest } = config
|
|
76
|
+
|
|
77
|
+
if (!type) {
|
|
78
|
+
throw new Error('[authFactory] Config object requires "type" property')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Merge built-in types with custom types from context
|
|
82
|
+
const allTypes = { ...authTypes, ...context.authTypes }
|
|
83
|
+
const AdapterClass = allTypes[type]
|
|
84
|
+
|
|
85
|
+
if (!AdapterClass) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[authFactory] Unknown auth type: "${type}". ` +
|
|
88
|
+
`Available: ${Object.keys(allTypes).join(', ')}`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new AdapterClass(rest)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if config is a composite auth config
|
|
97
|
+
*/
|
|
98
|
+
function isCompositeConfig(config: unknown): config is CompositeAuthConfig {
|
|
99
|
+
return (
|
|
100
|
+
config !== null &&
|
|
101
|
+
typeof config === 'object' &&
|
|
102
|
+
'default' in config &&
|
|
103
|
+
!('type' in config)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Auth factory input types
|
|
109
|
+
*/
|
|
110
|
+
export type AuthFactoryInput =
|
|
111
|
+
| EntityAuthAdapter
|
|
112
|
+
| ((options?: EntityAuthAdapterOptions) => AuthUser | null)
|
|
113
|
+
| string
|
|
114
|
+
| CompositeAuthConfig
|
|
115
|
+
| AuthConfig
|
|
116
|
+
| null
|
|
117
|
+
| undefined
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Auth factory - normalizes input and resolves to adapter
|
|
121
|
+
*
|
|
122
|
+
* Handles:
|
|
123
|
+
* - AuthAdapter instance → return directly (backward compatible)
|
|
124
|
+
* - Function → wrap as EntityAuthAdapter with getCurrentUser callback
|
|
125
|
+
* - String pattern 'type' → parse and resolve
|
|
126
|
+
* - Config object with 'type' → resolve via registry
|
|
127
|
+
* - Config object with 'default' → create CompositeAuthAdapter
|
|
128
|
+
*/
|
|
129
|
+
export function authFactory(
|
|
130
|
+
config: AuthFactoryInput,
|
|
131
|
+
context: AuthFactoryContext = {}
|
|
132
|
+
): EntityAuthAdapter {
|
|
133
|
+
const { authResolver = defaultAuthResolver } = context
|
|
134
|
+
|
|
135
|
+
// Null/undefined → permissive (safe default)
|
|
136
|
+
if (config == null) {
|
|
137
|
+
return new PermissiveAuthAdapter()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Function → wrap in EntityAuthAdapter with getCurrentUser callback
|
|
141
|
+
if (typeof config === 'function') {
|
|
142
|
+
return new EntityAuthAdapter({ getCurrentUser: config as () => AuthUser | null })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Already an EntityAuthAdapter instance → return directly (backward compatible)
|
|
146
|
+
if (config instanceof EntityAuthAdapter) {
|
|
147
|
+
return config
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Duck-typed adapter (has canPerform method)
|
|
151
|
+
if (
|
|
152
|
+
typeof config === 'object' &&
|
|
153
|
+
'canPerform' in config &&
|
|
154
|
+
typeof config.canPerform === 'function'
|
|
155
|
+
) {
|
|
156
|
+
return config as unknown as EntityAuthAdapter
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// String pattern → parse and resolve
|
|
160
|
+
if (typeof config === 'string') {
|
|
161
|
+
const parsed = parseAuthPattern(config)
|
|
162
|
+
if (!parsed) {
|
|
163
|
+
throw new Error(`[authFactory] Invalid auth pattern: "${config}"`)
|
|
164
|
+
}
|
|
165
|
+
return authResolver(parsed, context)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Config object
|
|
169
|
+
if (typeof config === 'object') {
|
|
170
|
+
// Composite config: { default: ..., mapping: { ... } }
|
|
171
|
+
if (isCompositeConfig(config)) {
|
|
172
|
+
const CompositeClass = context.authTypes?.composite || context.CompositeAuthAdapter
|
|
173
|
+
if (!CompositeClass) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'[authFactory] Composite config requires CompositeAuthAdapter. ' +
|
|
176
|
+
'Pass it via context.CompositeAuthAdapter or register as context.authTypes.composite'
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
return new CompositeClass(config, context)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Simple config with type: { type: 'jwt', ... }
|
|
183
|
+
if ('type' in config && config.type) {
|
|
184
|
+
return authResolver(config as AuthConfig, context)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error('[authFactory] Config object requires either "type" or "default" property')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[authFactory] Invalid auth config: ${typeof config}. ` +
|
|
192
|
+
'Expected AuthAdapter instance, string, or config object.'
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a custom auth factory with bound context
|
|
198
|
+
*/
|
|
199
|
+
export function createAuthFactory(
|
|
200
|
+
context: AuthFactoryContext
|
|
201
|
+
): (config: AuthFactoryInput) => EntityAuthAdapter {
|
|
202
|
+
return (config) => authFactory(config, context)
|
|
203
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Module
|
|
3
|
+
*
|
|
4
|
+
* AuthAdapter interface and implementations for scope/silo permission checks.
|
|
5
|
+
* Includes Symfony-inspired role hierarchy and security checker.
|
|
6
|
+
*
|
|
7
|
+
* Multi-source auth:
|
|
8
|
+
* - CompositeAuthAdapter routes to different adapters based on entity patterns
|
|
9
|
+
* - authFactory normalizes config (instance, string, or object)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Interface
|
|
13
|
+
export { EntityAuthAdapter, AuthActions } from './EntityAuthAdapter'
|
|
14
|
+
export type { AuthUser, EntityAuthAdapterOptions, AuthAction } from './EntityAuthAdapter'
|
|
15
|
+
|
|
16
|
+
// Role Hierarchy (topological resolution)
|
|
17
|
+
export { RoleHierarchy, createRoleHierarchy } from './RoleHierarchy'
|
|
18
|
+
export type { RoleHierarchyConfig } from './RoleHierarchy'
|
|
19
|
+
|
|
20
|
+
// Security Checker (isGranted contract)
|
|
21
|
+
export { SecurityChecker, createSecurityChecker } from './SecurityChecker'
|
|
22
|
+
export type {
|
|
23
|
+
RoleProvider,
|
|
24
|
+
SecurityCheckerOptions,
|
|
25
|
+
SecurityCheckerConfig,
|
|
26
|
+
} from './SecurityChecker'
|
|
27
|
+
|
|
28
|
+
// Implementations
|
|
29
|
+
export { PermissiveAuthAdapter, createPermissiveAdapter } from './PermissiveAdapter'
|
|
30
|
+
export { CompositeAuthAdapter, createCompositeAuthAdapter } from './CompositeAuthAdapter'
|
|
31
|
+
export type { CompositeAuthConfig } from './CompositeAuthAdapter'
|
|
32
|
+
|
|
33
|
+
// Factory/Resolver pattern (like storage/factory.ts)
|
|
34
|
+
export {
|
|
35
|
+
authFactory,
|
|
36
|
+
createAuthFactory,
|
|
37
|
+
authTypes,
|
|
38
|
+
parseAuthPattern,
|
|
39
|
+
defaultAuthResolver,
|
|
40
|
+
} from './factory'
|
|
41
|
+
export type {
|
|
42
|
+
ParsedAuthPattern,
|
|
43
|
+
AuthConfig,
|
|
44
|
+
AuthFactoryContext,
|
|
45
|
+
AuthResolver,
|
|
46
|
+
AuthFactoryInput,
|
|
47
|
+
} from './factory'
|