@jmruthers/pace-core 0.5.110 → 0.5.111
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/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +13 -8
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +36 -36
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +3 -3
- package/docs/rbac/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +37 -13
- package/src/rbac/api.ts +25 -8
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +4 -3
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +12 -2
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +22 -7
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3J5N2T2N.js.map +0 -1
- package/dist/chunk-7GBEBJLR.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-HADXAZT3.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-XRSP3H52.js.map +0 -1
- /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
|
@@ -268,8 +268,8 @@ describe('usePermissions Hook', () => {
|
|
|
268
268
|
'create:users': true,
|
|
269
269
|
'update:users': true,
|
|
270
270
|
'delete:users': true,
|
|
271
|
-
'
|
|
272
|
-
'
|
|
271
|
+
'update:organisations': true,
|
|
272
|
+
'update:events': true,
|
|
273
273
|
'super_admin': true
|
|
274
274
|
};
|
|
275
275
|
|
|
@@ -287,7 +287,7 @@ describe('usePermissions Hook', () => {
|
|
|
287
287
|
expect(result.current.hasPermission('create:users')).toBe(true);
|
|
288
288
|
expect(result.current.hasPermission('update:users')).toBe(true);
|
|
289
289
|
expect(result.current.hasPermission('delete:users')).toBe(true);
|
|
290
|
-
expect(result.current.hasPermission('
|
|
290
|
+
expect(result.current.hasPermission('update:organisations')).toBe(true);
|
|
291
291
|
expect(result.current.hasPermission('super_admin')).toBe(true);
|
|
292
292
|
});
|
|
293
293
|
|
|
@@ -297,8 +297,8 @@ describe('usePermissions Hook', () => {
|
|
|
297
297
|
'create:users': true,
|
|
298
298
|
'update:users': true,
|
|
299
299
|
'delete:users': false, // Org admin can't delete users
|
|
300
|
-
'
|
|
301
|
-
'
|
|
300
|
+
'update:organisations': true,
|
|
301
|
+
'update:events': true,
|
|
302
302
|
'org_admin': true
|
|
303
303
|
};
|
|
304
304
|
|
|
@@ -316,7 +316,7 @@ describe('usePermissions Hook', () => {
|
|
|
316
316
|
expect(result.current.hasPermission('create:users')).toBe(true);
|
|
317
317
|
expect(result.current.hasPermission('update:users')).toBe(true);
|
|
318
318
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
319
|
-
expect(result.current.hasPermission('
|
|
319
|
+
expect(result.current.hasPermission('update:organisations')).toBe(true);
|
|
320
320
|
expect(result.current.hasPermission('org_admin')).toBe(true);
|
|
321
321
|
});
|
|
322
322
|
|
|
@@ -326,8 +326,8 @@ describe('usePermissions Hook', () => {
|
|
|
326
326
|
'create:users': false,
|
|
327
327
|
'update:users': false,
|
|
328
328
|
'delete:users': false,
|
|
329
|
-
'
|
|
330
|
-
'
|
|
329
|
+
'update:organisations': false,
|
|
330
|
+
'update:events': true,
|
|
331
331
|
'event_admin': true
|
|
332
332
|
};
|
|
333
333
|
|
|
@@ -345,8 +345,8 @@ describe('usePermissions Hook', () => {
|
|
|
345
345
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
346
346
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
347
347
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
348
|
-
expect(result.current.hasPermission('
|
|
349
|
-
expect(result.current.hasPermission('
|
|
348
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
349
|
+
expect(result.current.hasPermission('update:events')).toBe(true);
|
|
350
350
|
expect(result.current.hasPermission('event_admin')).toBe(true);
|
|
351
351
|
});
|
|
352
352
|
|
|
@@ -356,8 +356,8 @@ describe('usePermissions Hook', () => {
|
|
|
356
356
|
'create:users': false,
|
|
357
357
|
'update:users': false,
|
|
358
358
|
'delete:users': false,
|
|
359
|
-
'
|
|
360
|
-
'
|
|
359
|
+
'update:organisations': false,
|
|
360
|
+
'update:events': false,
|
|
361
361
|
'member': true
|
|
362
362
|
};
|
|
363
363
|
|
|
@@ -375,8 +375,8 @@ describe('usePermissions Hook', () => {
|
|
|
375
375
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
376
376
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
377
377
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
378
|
-
expect(result.current.hasPermission('
|
|
379
|
-
expect(result.current.hasPermission('
|
|
378
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
379
|
+
expect(result.current.hasPermission('update:events')).toBe(false);
|
|
380
380
|
expect(result.current.hasPermission('member')).toBe(true);
|
|
381
381
|
});
|
|
382
382
|
|
|
@@ -386,8 +386,8 @@ describe('usePermissions Hook', () => {
|
|
|
386
386
|
'create:users': false,
|
|
387
387
|
'update:users': false,
|
|
388
388
|
'delete:users': false,
|
|
389
|
-
'
|
|
390
|
-
'
|
|
389
|
+
'update:organisations': false,
|
|
390
|
+
'update:events': false,
|
|
391
391
|
'participant': true
|
|
392
392
|
};
|
|
393
393
|
|
|
@@ -405,8 +405,8 @@ describe('usePermissions Hook', () => {
|
|
|
405
405
|
expect(result.current.hasPermission('create:users')).toBe(false);
|
|
406
406
|
expect(result.current.hasPermission('update:users')).toBe(false);
|
|
407
407
|
expect(result.current.hasPermission('delete:users')).toBe(false);
|
|
408
|
-
expect(result.current.hasPermission('
|
|
409
|
-
expect(result.current.hasPermission('
|
|
408
|
+
expect(result.current.hasPermission('update:organisations')).toBe(false);
|
|
409
|
+
expect(result.current.hasPermission('update:events')).toBe(false);
|
|
410
410
|
expect(result.current.hasPermission('participant')).toBe(true);
|
|
411
411
|
});
|
|
412
412
|
|
|
@@ -416,8 +416,8 @@ describe('usePermissions Hook', () => {
|
|
|
416
416
|
'create:users': false,
|
|
417
417
|
'update:users': true,
|
|
418
418
|
'delete:users': false,
|
|
419
|
-
'
|
|
420
|
-
'
|
|
419
|
+
'update:organisations': false,
|
|
420
|
+
'update:events': true
|
|
421
421
|
};
|
|
422
422
|
|
|
423
423
|
mockGetPermissionMap.mockResolvedValue(mixedPermissions);
|
|
@@ -432,8 +432,8 @@ describe('usePermissions Hook', () => {
|
|
|
432
432
|
// Test hasAnyPermission with mixed results
|
|
433
433
|
expect(result.current.hasAnyPermission(['read:users', 'create:users'])).toBe(true); // One is true
|
|
434
434
|
expect(result.current.hasAnyPermission(['create:users', 'delete:users'])).toBe(false); // Both are false
|
|
435
|
-
expect(result.current.hasAnyPermission(['update:users', '
|
|
436
|
-
expect(result.current.hasAnyPermission(['read:users', 'update:users', '
|
|
435
|
+
expect(result.current.hasAnyPermission(['update:users', 'update:events'])).toBe(true); // Both are true
|
|
436
|
+
expect(result.current.hasAnyPermission(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
|
|
437
437
|
});
|
|
438
438
|
|
|
439
439
|
it('handles mixed permission scenarios with hasAllPermissions', async () => {
|
|
@@ -442,8 +442,8 @@ describe('usePermissions Hook', () => {
|
|
|
442
442
|
'create:users': false,
|
|
443
443
|
'update:users': true,
|
|
444
444
|
'delete:users': false,
|
|
445
|
-
'
|
|
446
|
-
'
|
|
445
|
+
'update:organisations': false,
|
|
446
|
+
'update:events': true
|
|
447
447
|
};
|
|
448
448
|
|
|
449
449
|
mockGetPermissionMap.mockResolvedValue(mixedPermissions);
|
|
@@ -459,7 +459,7 @@ describe('usePermissions Hook', () => {
|
|
|
459
459
|
expect(result.current.hasAllPermissions(['read:users', 'update:users'])).toBe(true); // Both are true
|
|
460
460
|
expect(result.current.hasAllPermissions(['read:users', 'create:users'])).toBe(false); // One is false
|
|
461
461
|
expect(result.current.hasAllPermissions(['create:users', 'delete:users'])).toBe(false); // Both are false
|
|
462
|
-
expect(result.current.hasAllPermissions(['read:users', 'update:users', '
|
|
462
|
+
expect(result.current.hasAllPermissions(['read:users', 'update:users', 'update:events'])).toBe(true); // All are true
|
|
463
463
|
});
|
|
464
464
|
});
|
|
465
465
|
|
|
@@ -52,6 +52,22 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
52
52
|
const [error, setError] = useState<Error | null>(null);
|
|
53
53
|
const isFetchingRef = useRef(false);
|
|
54
54
|
|
|
55
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
62
|
+
|
|
63
|
+
return () => clearTimeout(timeoutId);
|
|
64
|
+
}
|
|
65
|
+
// Clear error if organisation context becomes available
|
|
66
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
67
|
+
setError(null);
|
|
68
|
+
}
|
|
69
|
+
}, [scope.organisationId, error]);
|
|
70
|
+
|
|
55
71
|
useEffect(() => {
|
|
56
72
|
const fetchPermissions = async () => {
|
|
57
73
|
// Prevent multiple simultaneous fetches
|
|
@@ -65,9 +81,9 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
65
81
|
return;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
|
|
69
|
-
//
|
|
70
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
84
|
+
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
85
|
+
// Wait for organisation context to resolve
|
|
86
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
71
87
|
setPermissions({} as PermissionMap);
|
|
72
88
|
setIsLoading(true);
|
|
73
89
|
setError(null);
|
|
@@ -125,8 +141,8 @@ export function usePermissions(userId: UUID, scope: Scope) {
|
|
|
125
141
|
return;
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
// Don't fetch permissions if scope is invalid (e.g., organisationId is empty)
|
|
129
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
144
|
+
// Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
145
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
130
146
|
setPermissions({} as PermissionMap);
|
|
131
147
|
setIsLoading(true);
|
|
132
148
|
setError(null);
|
|
@@ -193,6 +209,23 @@ export function useCan(
|
|
|
193
209
|
const [isLoading, setIsLoading] = useState(true);
|
|
194
210
|
const [error, setError] = useState<Error | null>(null);
|
|
195
211
|
|
|
212
|
+
// Add timeout for missing organisation context (3 seconds)
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
215
|
+
const timeoutId = setTimeout(() => {
|
|
216
|
+
setError(new Error('Organisation context is required for permission checks'));
|
|
217
|
+
setIsLoading(false);
|
|
218
|
+
setCan(false);
|
|
219
|
+
}, 3000); // 3 seconds - typical permission check is < 1 second
|
|
220
|
+
|
|
221
|
+
return () => clearTimeout(timeoutId);
|
|
222
|
+
}
|
|
223
|
+
// Clear error if organisation context becomes available
|
|
224
|
+
if (error?.message === 'Organisation context is required for permission checks') {
|
|
225
|
+
setError(null);
|
|
226
|
+
}
|
|
227
|
+
}, [scope.organisationId, error]);
|
|
228
|
+
|
|
196
229
|
// Use refs to track the last values to prevent unnecessary re-runs
|
|
197
230
|
const lastUserIdRef = useRef<UUID | null>(null);
|
|
198
231
|
const lastScopeRef = useRef<string | null>(null);
|
|
@@ -226,12 +259,13 @@ export function useCan(
|
|
|
226
259
|
return;
|
|
227
260
|
}
|
|
228
261
|
|
|
229
|
-
// Don't check permissions if scope is invalid (e.g., organisationId is empty)
|
|
230
|
-
//
|
|
231
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
232
|
-
console.log('[useCan] Invalid scope - organisationId is empty:', { scope, permission, pageId });
|
|
233
|
-
setCan(false);
|
|
262
|
+
// Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
263
|
+
// Wait for organisation context to resolve
|
|
264
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
234
265
|
setIsLoading(true);
|
|
266
|
+
setCan(false);
|
|
267
|
+
setError(null);
|
|
268
|
+
// Timeout is handled in separate useEffect (Phase 1.4)
|
|
235
269
|
return;
|
|
236
270
|
}
|
|
237
271
|
|
|
@@ -239,21 +273,10 @@ export function useCan(
|
|
|
239
273
|
setIsLoading(true);
|
|
240
274
|
setError(null);
|
|
241
275
|
|
|
242
|
-
console.log('[useCan] Checking permission:', {
|
|
243
|
-
userId,
|
|
244
|
-
organisationId: scope.organisationId,
|
|
245
|
-
eventId: scope.eventId,
|
|
246
|
-
appId: scope.appId,
|
|
247
|
-
permission,
|
|
248
|
-
pageId
|
|
249
|
-
});
|
|
250
|
-
|
|
251
276
|
const result = useCache
|
|
252
277
|
? await isPermittedCached({ userId, scope, permission, pageId })
|
|
253
278
|
: await isPermitted({ userId, scope, permission, pageId });
|
|
254
279
|
|
|
255
|
-
console.log('[useCan] Permission check result:', { permission, result, pageId });
|
|
256
|
-
|
|
257
280
|
setCan(result);
|
|
258
281
|
} catch (err) {
|
|
259
282
|
console.error('[useCan] Permission check error:', { permission, error: err });
|
|
@@ -275,10 +298,11 @@ export function useCan(
|
|
|
275
298
|
return;
|
|
276
299
|
}
|
|
277
300
|
|
|
278
|
-
// Don't check permissions if scope is invalid (e.g., organisationId is empty)
|
|
279
|
-
if (!scope.organisationId || scope.organisationId.trim() === '') {
|
|
301
|
+
// Don't check permissions if scope is invalid (e.g., organisationId is null/empty)
|
|
302
|
+
if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
|
|
280
303
|
setCan(false);
|
|
281
304
|
setIsLoading(true);
|
|
305
|
+
setError(null);
|
|
282
306
|
return;
|
|
283
307
|
}
|
|
284
308
|
|
|
@@ -57,7 +57,6 @@ vi.mock('./useRBAC', () => ({
|
|
|
57
57
|
eventAppRole: 'participant',
|
|
58
58
|
isSuperAdmin: false,
|
|
59
59
|
isOrgAdmin: false,
|
|
60
|
-
hasPermission: () => true,
|
|
61
60
|
hasGlobalPermission: () => true,
|
|
62
61
|
error: null,
|
|
63
62
|
}),
|
|
@@ -80,18 +79,12 @@ describe('useRBAC Hook - Simple Tests', () => {
|
|
|
80
79
|
expect(result.current).toHaveProperty('globalRole');
|
|
81
80
|
expect(result.current).toHaveProperty('organisationRole');
|
|
82
81
|
expect(result.current).toHaveProperty('eventAppRole');
|
|
83
|
-
expect(result.current).toHaveProperty('hasPermission');
|
|
84
82
|
expect(result.current).toHaveProperty('hasGlobalPermission');
|
|
85
83
|
expect(result.current).toHaveProperty('isSuperAdmin');
|
|
86
84
|
expect(result.current).toHaveProperty('isOrgAdmin');
|
|
87
85
|
expect(result.current).toHaveProperty('isLoading');
|
|
88
86
|
expect(result.current).toHaveProperty('error');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
it('hasPermission is a function', () => {
|
|
92
|
-
const { result } = renderHook(() => useRBAC());
|
|
93
|
-
|
|
94
|
-
expect(typeof result.current.hasPermission).toBe('function');
|
|
87
|
+
// Note: hasPermission was removed - use useCan() hook instead
|
|
95
88
|
});
|
|
96
89
|
|
|
97
90
|
it('hasGlobalPermission is a function', () => {
|
|
@@ -97,7 +97,7 @@ describe('useRBAC', () => {
|
|
|
97
97
|
|
|
98
98
|
await waitFor(() => {
|
|
99
99
|
expect(result.current.isLoading).toBe(false);
|
|
100
|
-
expect(result.current.
|
|
100
|
+
expect(result.current.hasGlobalPermission).toBeTypeOf('function');
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
expect(mockResolveAppContext).toHaveBeenCalledWith({ userId: 'user-1', appName: 'test-app' });
|
|
@@ -109,45 +109,8 @@ describe('useRBAC', () => {
|
|
|
109
109
|
);
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
user: { id: 'user-1' },
|
|
115
|
-
session: { access_token: 'token' },
|
|
116
|
-
appName: 'test-app',
|
|
117
|
-
});
|
|
118
|
-
mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
|
|
119
|
-
mockGetPermissionMap.mockResolvedValue({ 'read:dashboard': true });
|
|
120
|
-
|
|
121
|
-
const { result } = renderHook(() => useRBAC('dashboard'));
|
|
122
|
-
|
|
123
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
124
|
-
|
|
125
|
-
mockIsPermittedCached.mockClear();
|
|
126
|
-
const allowed = await result.current.hasPermission('read', 'dashboard');
|
|
127
|
-
|
|
128
|
-
expect(allowed).toBe(true);
|
|
129
|
-
expect(mockIsPermittedCached).not.toHaveBeenCalled();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('falls back to engine when permission not cached', async () => {
|
|
133
|
-
mockUseUnifiedAuth.mockReturnValue({
|
|
134
|
-
user: { id: 'user-1' },
|
|
135
|
-
session: { access_token: 'token' },
|
|
136
|
-
appName: 'test-app',
|
|
137
|
-
});
|
|
138
|
-
mockUseOrganisations.mockReturnValue({ selectedOrganisation: { id: 'org-1' } });
|
|
139
|
-
mockGetPermissionMap.mockResolvedValue({});
|
|
140
|
-
mockIsPermittedCached.mockResolvedValue(true);
|
|
141
|
-
|
|
142
|
-
const { result } = renderHook(() => useRBAC('dashboard'));
|
|
143
|
-
|
|
144
|
-
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
145
|
-
|
|
146
|
-
const allowed = await result.current.hasPermission('read', 'dashboard');
|
|
147
|
-
|
|
148
|
-
expect(allowed).toBe(true);
|
|
149
|
-
expect(mockIsPermittedCached).toHaveBeenCalled();
|
|
150
|
-
});
|
|
112
|
+
// Note: Permission checking tests have been moved to useCan.test.ts
|
|
113
|
+
// useRBAC now focuses on role information and context, not permission checks
|
|
151
114
|
|
|
152
115
|
it('handles denied app resolution securely', async () => {
|
|
153
116
|
mockUseUnifiedAuth.mockReturnValue({
|
|
@@ -17,7 +17,6 @@ import { useEvents } from '../../hooks/useEvents';
|
|
|
17
17
|
import {
|
|
18
18
|
getPermissionMap,
|
|
19
19
|
getAccessLevel,
|
|
20
|
-
isPermittedCached,
|
|
21
20
|
resolveAppContext,
|
|
22
21
|
getRoleContext,
|
|
23
22
|
} from '../api';
|
|
@@ -27,7 +26,6 @@ import type {
|
|
|
27
26
|
GlobalRole,
|
|
28
27
|
OrganisationRole,
|
|
29
28
|
EventAppRole,
|
|
30
|
-
Operation,
|
|
31
29
|
Permission,
|
|
32
30
|
Scope,
|
|
33
31
|
PermissionMap,
|
|
@@ -125,58 +123,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
125
123
|
}
|
|
126
124
|
}, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user]);
|
|
127
125
|
|
|
128
|
-
const hasPermission = useCallback(
|
|
129
|
-
async (operationOrPermission: Operation | string, targetPageId?: string): Promise<boolean> => {
|
|
130
|
-
if (!user) {
|
|
131
|
-
logger.warn('[useRBAC] Permission check attempted without authenticated user context');
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!currentScope || !currentScope.organisationId) {
|
|
136
|
-
logger.error('[useRBAC] Permission check denied due to missing organisation context', {
|
|
137
|
-
hasScope: !!currentScope,
|
|
138
|
-
scope: currentScope,
|
|
139
|
-
userId: user.id,
|
|
140
|
-
permission: operationOrPermission,
|
|
141
|
-
});
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (globalRole === 'super_admin' || permissionMap['*']) {
|
|
146
|
-
logger.info('[useRBAC] Super admin bypass granted', {
|
|
147
|
-
userId: user.id,
|
|
148
|
-
permission: operationOrPermission,
|
|
149
|
-
});
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let permission: Permission;
|
|
154
|
-
if (operationOrPermission.includes(':')) {
|
|
155
|
-
permission = operationOrPermission as Permission;
|
|
156
|
-
} else if (targetPageId || pageId) {
|
|
157
|
-
permission = `${operationOrPermission}:${targetPageId || pageId}` as Permission;
|
|
158
|
-
} else {
|
|
159
|
-
permission = operationOrPermission as Permission;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const cachedValue = permissionMap[permission];
|
|
163
|
-
if (cachedValue === true) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
if (cachedValue === false) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return isPermittedCached({
|
|
171
|
-
userId: user.id as UUID,
|
|
172
|
-
scope: currentScope,
|
|
173
|
-
permission,
|
|
174
|
-
pageId: targetPageId ?? pageId,
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
|
-
[currentScope, globalRole, logger, pageId, permissionMap, user],
|
|
178
|
-
);
|
|
179
|
-
|
|
180
126
|
const hasGlobalPermission = useCallback(
|
|
181
127
|
(permission: string): boolean => {
|
|
182
128
|
if (globalRole === 'super_admin' || permissionMap['*']) {
|
|
@@ -211,7 +157,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
|
|
|
211
157
|
globalRole,
|
|
212
158
|
organisationRole,
|
|
213
159
|
eventAppRole,
|
|
214
|
-
hasPermission,
|
|
215
160
|
hasGlobalPermission,
|
|
216
161
|
isSuperAdmin,
|
|
217
162
|
isOrgAdmin,
|
|
@@ -74,28 +74,30 @@ export function useResolvedScope({
|
|
|
74
74
|
eventId: undefined
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Only update if the scope has actually changed
|
|
86
|
-
if (stableScopeRef.current.organisationId !== newScope.organisationId ||
|
|
87
|
-
stableScopeRef.current.eventId !== newScope.eventId ||
|
|
88
|
-
stableScopeRef.current.appId !== newScope.appId) {
|
|
89
|
-
stableScopeRef.current = {
|
|
90
|
-
organisationId: newScope.organisationId,
|
|
91
|
-
appId: newScope.appId || '',
|
|
92
|
-
eventId: newScope.eventId
|
|
77
|
+
// Update stable scope ref in useEffect to avoid updates during render
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (resolvedScope && resolvedScope.organisationId) {
|
|
80
|
+
const newScope = {
|
|
81
|
+
organisationId: resolvedScope.organisationId,
|
|
82
|
+
appId: resolvedScope.appId,
|
|
83
|
+
eventId: resolvedScope.eventId
|
|
93
84
|
};
|
|
85
|
+
|
|
86
|
+
// Only update if the scope has actually changed
|
|
87
|
+
if (stableScopeRef.current.organisationId !== newScope.organisationId ||
|
|
88
|
+
stableScopeRef.current.eventId !== newScope.eventId ||
|
|
89
|
+
stableScopeRef.current.appId !== newScope.appId) {
|
|
90
|
+
stableScopeRef.current = {
|
|
91
|
+
organisationId: newScope.organisationId,
|
|
92
|
+
appId: newScope.appId || '',
|
|
93
|
+
eventId: newScope.eventId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} else if (!resolvedScope) {
|
|
97
|
+
// Reset to empty scope when no resolved scope
|
|
98
|
+
stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
|
|
94
99
|
}
|
|
95
|
-
}
|
|
96
|
-
// Reset to empty scope when no resolved scope
|
|
97
|
-
stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
|
|
98
|
-
}
|
|
100
|
+
}, [resolvedScope]);
|
|
99
101
|
|
|
100
102
|
const stableScope = stableScopeRef.current;
|
|
101
103
|
|
|
@@ -144,17 +146,10 @@ export function useResolvedScope({
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
//
|
|
148
|
-
console.log('[useResolvedScope] Attempting to resolve scope:', {
|
|
149
|
-
selectedOrganisationId,
|
|
150
|
-
selectedEventId,
|
|
151
|
-
appId: appId || 'NOT RESOLVED YET',
|
|
152
|
-
resolveStep: 'initial'
|
|
153
|
-
});
|
|
149
|
+
// Resolve scope based on available context
|
|
154
150
|
|
|
155
151
|
// If we have both organisation and event, use them directly
|
|
156
152
|
if (selectedOrganisationId && selectedEventId) {
|
|
157
|
-
console.log('[useResolvedScope] Resolving with both org and event');
|
|
158
153
|
if (!cancelled) {
|
|
159
154
|
setResolvedScope({
|
|
160
155
|
organisationId: selectedOrganisationId,
|
|
@@ -168,7 +163,6 @@ export function useResolvedScope({
|
|
|
168
163
|
|
|
169
164
|
// If we only have organisation, use it
|
|
170
165
|
if (selectedOrganisationId) {
|
|
171
|
-
console.log('[useResolvedScope] Resolving with organisation only');
|
|
172
166
|
if (!cancelled) {
|
|
173
167
|
setResolvedScope({
|
|
174
168
|
organisationId: selectedOrganisationId,
|
|
@@ -183,7 +177,6 @@ export function useResolvedScope({
|
|
|
183
177
|
// If we only have event, resolve organisation from event
|
|
184
178
|
if (selectedEventId && supabase) {
|
|
185
179
|
try {
|
|
186
|
-
console.log('[useResolvedScope] Resolving from event:', { selectedEventId, appId });
|
|
187
180
|
const eventScope = await createScopeFromEvent(supabase, selectedEventId, appId);
|
|
188
181
|
if (!eventScope) {
|
|
189
182
|
console.error('[useResolvedScope] Could not resolve organization from event context');
|
|
@@ -194,7 +187,6 @@ export function useResolvedScope({
|
|
|
194
187
|
}
|
|
195
188
|
return;
|
|
196
189
|
}
|
|
197
|
-
console.log('[useResolvedScope] Resolved from event:', eventScope);
|
|
198
190
|
// Preserve the resolved app ID
|
|
199
191
|
if (!cancelled) {
|
|
200
192
|
setResolvedScope({
|
|
@@ -84,13 +84,17 @@ describe('RBAC Permissions', () => {
|
|
|
84
84
|
|
|
85
85
|
it('rejects invalid Permission types', () => {
|
|
86
86
|
const invalidPermissions = [
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
':users',
|
|
91
|
-
'read
|
|
92
|
-
'
|
|
93
|
-
'
|
|
87
|
+
'READ:users', // Capital letters not allowed
|
|
88
|
+
'read:', // Missing resource
|
|
89
|
+
':users', // Missing operation
|
|
90
|
+
'read:users*', // Invalid wildcard placement
|
|
91
|
+
'read:*users', // Invalid wildcard placement
|
|
92
|
+
'invalid', // Not in format operation:resource
|
|
93
|
+
'read:users-', // Invalid character (hyphen) at end
|
|
94
|
+
'read:users_', // Invalid character (underscore) at end
|
|
95
|
+
'read:.users', // Cannot start with dot
|
|
96
|
+
'read:users.', // Cannot end with dot
|
|
97
|
+
'read:users..detail', // Cannot have consecutive dots
|
|
94
98
|
];
|
|
95
99
|
|
|
96
100
|
invalidPermissions.forEach(permission => {
|
|
@@ -23,7 +23,7 @@ describe('RBACSecurityValidator', () => {
|
|
|
23
23
|
'create:user-profiles',
|
|
24
24
|
'update:events',
|
|
25
25
|
'delete:organisations',
|
|
26
|
-
'
|
|
26
|
+
'update:base.events',
|
|
27
27
|
'read:user-profiles.details'
|
|
28
28
|
];
|
|
29
29
|
|
|
@@ -352,7 +352,7 @@ describe('RBACSecurityMiddleware', () => {
|
|
|
352
352
|
const input = {
|
|
353
353
|
userId: '123e4567-e89b-12d3-a456-426614174000' as UUID,
|
|
354
354
|
scope: { organisationId: '123e4567-e89b-12d3-a456-426614174001' as UUID },
|
|
355
|
-
permission: '
|
|
355
|
+
permission: 'update:everything' as Permission
|
|
356
356
|
};
|
|
357
357
|
|
|
358
358
|
const context = {
|
package/src/rbac/security.ts
CHANGED
|
@@ -24,7 +24,9 @@ export class RBACSecurityValidator {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Permission format: operation:resource[.subresource]
|
|
27
|
-
|
|
27
|
+
// Only CRUD operations are allowed (read, create, update, delete)
|
|
28
|
+
// The 'manage' operation has been removed for RBAC compliance
|
|
29
|
+
const permissionRegex = /^(read|create|update|delete):[a-z0-9._-]+$/;
|
|
28
30
|
return permissionRegex.test(permission);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -53,9 +55,15 @@ export class RBACSecurityValidator {
|
|
|
53
55
|
return false;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
// Organisation ID
|
|
57
|
-
if (scope.organisationId
|
|
58
|
-
|
|
58
|
+
// Organisation ID validation - reject empty strings
|
|
59
|
+
if (scope.organisationId !== undefined) {
|
|
60
|
+
// Reject empty strings - use undefined/null instead
|
|
61
|
+
if (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (scope.organisationId && !this.validateUUID(scope.organisationId)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
// Event ID should be a string if provided
|
|
@@ -122,7 +130,8 @@ export class RBACSecurityValidator {
|
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
const [operation] = permission.split(':');
|
|
125
|
-
|
|
133
|
+
// Only CRUD operations - 'manage' has been removed
|
|
134
|
+
const hierarchy = ['read', 'create', 'update', 'delete'];
|
|
126
135
|
|
|
127
136
|
const permissionLevel = hierarchy.indexOf(operation);
|
|
128
137
|
const requiredLevel = hierarchy.indexOf(requiredOperation);
|
|
@@ -257,10 +266,13 @@ export const DEFAULT_SECURITY_CONFIG: RBACSecurityConfig = {
|
|
|
257
266
|
|
|
258
267
|
/**
|
|
259
268
|
* Security context for RBAC operations
|
|
269
|
+
*
|
|
270
|
+
* OrganisationId is required - it can always be derived from event context in event-based apps.
|
|
271
|
+
* If organisation context is not available, the operation should fail rather than proceed without context.
|
|
260
272
|
*/
|
|
261
273
|
export interface SecurityContext {
|
|
262
274
|
userId: UUID;
|
|
263
|
-
organisationId
|
|
275
|
+
organisationId: UUID; // Required - can always be derived from event context
|
|
264
276
|
ipAddress?: string;
|
|
265
277
|
userAgent?: string;
|
|
266
278
|
timestamp: Date;
|
|
@@ -304,7 +316,10 @@ export class RBACSecurityMiddleware {
|
|
|
304
316
|
errors.push('Invalid user ID format');
|
|
305
317
|
}
|
|
306
318
|
|
|
307
|
-
|
|
319
|
+
// OrganisationId is required
|
|
320
|
+
if (!context.organisationId) {
|
|
321
|
+
errors.push('Organisation ID is required');
|
|
322
|
+
} else if (!RBACSecurityValidator.validateUUID(context.organisationId)) {
|
|
308
323
|
errors.push('Invalid organisation ID format');
|
|
309
324
|
}
|
|
310
325
|
|
package/src/rbac/types.test.ts
CHANGED
|
@@ -195,7 +195,7 @@ describe('Type Definitions', () => {
|
|
|
195
195
|
'create:user-profiles',
|
|
196
196
|
'update:events',
|
|
197
197
|
'delete:organisations',
|
|
198
|
-
'
|
|
198
|
+
'update:base.events'
|
|
199
199
|
];
|
|
200
200
|
|
|
201
201
|
permissions.forEach(permission => {
|
|
@@ -206,7 +206,7 @@ describe('Type Definitions', () => {
|
|
|
206
206
|
it('supports complex resource names', () => {
|
|
207
207
|
const permissions: Permission[] = [
|
|
208
208
|
'read:user-profiles.details',
|
|
209
|
-
'
|
|
209
|
+
'update:base.events.schedule',
|
|
210
210
|
'create:app-data.records'
|
|
211
211
|
];
|
|
212
212
|
|