@jmruthers/pace-core 0.5.185 → 0.5.187

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.
Files changed (300) hide show
  1. package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
  2. package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
  3. package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
  4. package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
  5. package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
  6. package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
  7. package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
  8. package/dist/chunk-3IC5WCMO.js.map +1 -0
  9. package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
  10. package/dist/chunk-3NFNJOO7.js.map +1 -0
  11. package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
  12. package/dist/chunk-63FOKYGO.js.map +1 -0
  13. package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
  14. package/dist/chunk-C4OYJOV4.js.map +1 -0
  15. package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
  16. package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
  17. package/dist/chunk-HEHYGYOX.js.map +1 -0
  18. package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
  19. package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
  20. package/dist/chunk-LBBUPSSC.js.map +1 -0
  21. package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
  22. package/dist/chunk-SAUPYVLF.js.map +1 -0
  23. package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
  24. package/dist/chunk-T6ZJVI3A.js.map +1 -0
  25. package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
  26. package/dist/chunk-ULX5FYEM.js.map +1 -0
  27. package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
  28. package/dist/chunk-WK2Y6TGA.js.map +1 -0
  29. package/dist/chunk-YHCN776L.js +447 -0
  30. package/dist/chunk-YHCN776L.js.map +1 -0
  31. package/dist/components.d.ts +4 -4
  32. package/dist/components.js +12 -10
  33. package/dist/components.js.map +1 -1
  34. package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
  35. package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
  36. package/dist/hooks.d.ts +265 -6
  37. package/dist/hooks.js +148 -49
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +25 -10
  40. package/dist/index.js +65 -30
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.js +1 -1
  43. package/dist/rbac/index.d.ts +125 -8
  44. package/dist/rbac/index.js +27 -7
  45. package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
  46. package/dist/types.d.ts +2 -2
  47. package/dist/types.js +1 -1
  48. package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
  49. package/dist/utils.d.ts +214 -4
  50. package/dist/utils.js +22 -2
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/classes/ColumnFactory.md +1 -1
  53. package/docs/api/classes/ErrorBoundary.md +1 -1
  54. package/docs/api/classes/InvalidScopeError.md +1 -1
  55. package/docs/api/classes/Logger.md +1 -1
  56. package/docs/api/classes/MissingUserContextError.md +1 -1
  57. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  58. package/docs/api/classes/PermissionDeniedError.md +1 -1
  59. package/docs/api/classes/RBACAuditManager.md +21 -17
  60. package/docs/api/classes/RBACCache.md +31 -23
  61. package/docs/api/classes/RBACEngine.md +6 -6
  62. package/docs/api/classes/RBACError.md +1 -1
  63. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  64. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  65. package/docs/api/classes/StorageUtils.md +1 -1
  66. package/docs/api/enums/FileCategory.md +1 -1
  67. package/docs/api/enums/LogLevel.md +1 -1
  68. package/docs/api/enums/RBACErrorCode.md +1 -1
  69. package/docs/api/enums/RPCFunction.md +1 -1
  70. package/docs/api/interfaces/AddressFieldProps.md +241 -0
  71. package/docs/api/interfaces/AddressFieldRef.md +94 -0
  72. package/docs/api/interfaces/AggregateConfig.md +1 -1
  73. package/docs/api/interfaces/AutocompleteOptions.md +75 -0
  74. package/docs/api/interfaces/BadgeProps.md +1 -1
  75. package/docs/api/interfaces/ButtonProps.md +1 -1
  76. package/docs/api/interfaces/CalendarProps.md +1 -1
  77. package/docs/api/interfaces/CardProps.md +1 -1
  78. package/docs/api/interfaces/ColorPalette.md +1 -1
  79. package/docs/api/interfaces/ColorShade.md +1 -1
  80. package/docs/api/interfaces/ComplianceResult.md +1 -1
  81. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  82. package/docs/api/interfaces/DataRecord.md +1 -1
  83. package/docs/api/interfaces/DataTableAction.md +1 -1
  84. package/docs/api/interfaces/DataTableColumn.md +1 -1
  85. package/docs/api/interfaces/DataTableProps.md +1 -1
  86. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  87. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  88. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  92. package/docs/api/interfaces/ExportColumn.md +1 -1
  93. package/docs/api/interfaces/ExportOptions.md +1 -1
  94. package/docs/api/interfaces/FileDisplayProps.md +15 -15
  95. package/docs/api/interfaces/FileMetadata.md +1 -1
  96. package/docs/api/interfaces/FileReference.md +1 -1
  97. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  98. package/docs/api/interfaces/FileUploadOptions.md +33 -9
  99. package/docs/api/interfaces/FileUploadProps.md +36 -14
  100. package/docs/api/interfaces/FooterProps.md +1 -1
  101. package/docs/api/interfaces/FormFieldProps.md +1 -1
  102. package/docs/api/interfaces/FormProps.md +1 -1
  103. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  104. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  105. package/docs/api/interfaces/InputProps.md +1 -1
  106. package/docs/api/interfaces/LabelProps.md +1 -1
  107. package/docs/api/interfaces/LoggerConfig.md +1 -1
  108. package/docs/api/interfaces/LoginFormProps.md +1 -1
  109. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  110. package/docs/api/interfaces/NavigationContextType.md +1 -1
  111. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  112. package/docs/api/interfaces/NavigationItem.md +1 -1
  113. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  114. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  115. package/docs/api/interfaces/Organisation.md +1 -1
  116. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  117. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  118. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  119. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  120. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  121. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  122. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  123. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  124. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  125. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  126. package/docs/api/interfaces/PaletteData.md +1 -1
  127. package/docs/api/interfaces/ParsedAddress.md +120 -0
  128. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  129. package/docs/api/interfaces/ProgressProps.md +1 -1
  130. package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
  131. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  132. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  134. package/docs/api/interfaces/QuickFix.md +1 -1
  135. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  136. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  137. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  138. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  139. package/docs/api/interfaces/RBACConfig.md +27 -4
  140. package/docs/api/interfaces/RBACContext.md +1 -1
  141. package/docs/api/interfaces/RBACLogger.md +5 -5
  142. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  143. package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
  144. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  148. package/docs/api/interfaces/RBACResult.md +1 -1
  149. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  151. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  153. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  155. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  157. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  159. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  160. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  161. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  162. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  163. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  164. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  165. package/docs/api/interfaces/RouteConfig.md +1 -1
  166. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  167. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  168. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  169. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  170. package/docs/api/interfaces/SetupIssue.md +1 -1
  171. package/docs/api/interfaces/StorageConfig.md +1 -1
  172. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  173. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  174. package/docs/api/interfaces/StorageListOptions.md +1 -1
  175. package/docs/api/interfaces/StorageListResult.md +1 -1
  176. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  177. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  178. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  179. package/docs/api/interfaces/StyleImport.md +1 -1
  180. package/docs/api/interfaces/SwitchProps.md +1 -1
  181. package/docs/api/interfaces/TabsContentProps.md +1 -1
  182. package/docs/api/interfaces/TabsListProps.md +1 -1
  183. package/docs/api/interfaces/TabsProps.md +1 -1
  184. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  185. package/docs/api/interfaces/TextareaProps.md +1 -1
  186. package/docs/api/interfaces/ToastActionElement.md +1 -1
  187. package/docs/api/interfaces/ToastProps.md +1 -1
  188. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  190. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  192. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  193. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  194. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  195. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  196. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  198. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  199. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  201. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  202. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  203. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  204. package/docs/api/interfaces/UserEventAccess.md +1 -1
  205. package/docs/api/interfaces/UserMenuProps.md +1 -1
  206. package/docs/api/interfaces/UserProfile.md +1 -1
  207. package/docs/api/modules.md +328 -69
  208. package/docs/api-reference/components.md +26 -12
  209. package/docs/best-practices/performance.md +11 -0
  210. package/docs/implementation-guides/file-reference-system.md +24 -2
  211. package/docs/implementation-guides/file-upload-storage.md +38 -1
  212. package/docs/rbac/README.md +2 -1
  213. package/docs/rbac/api-reference.md +11 -0
  214. package/docs/rbac/performance.md +320 -0
  215. package/docs/standards/01-architecture-standard.md +5 -0
  216. package/docs/standards/05-security-standard.md +12 -0
  217. package/package.json +1 -1
  218. package/scripts/check-pace-core-compliance.js +512 -0
  219. package/src/components/AddressField/AddressField.test.tsx +411 -0
  220. package/src/components/AddressField/AddressField.tsx +323 -0
  221. package/src/components/AddressField/README.md +336 -0
  222. package/src/components/AddressField/index.ts +10 -0
  223. package/src/components/AddressField/types.ts +65 -0
  224. package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
  225. package/src/components/FileDisplay/FileDisplay.tsx +28 -1
  226. package/src/components/FileUpload/FileUpload.test.tsx +2 -0
  227. package/src/components/FileUpload/FileUpload.tsx +7 -1
  228. package/src/components/Header/Header.tsx +2 -5
  229. package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
  230. package/src/components/index.ts +2 -0
  231. package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
  232. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
  233. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
  234. package/src/hooks/index.ts +9 -0
  235. package/src/hooks/public/usePublicFileDisplay.ts +8 -10
  236. package/src/hooks/useAddressAutocomplete.test.ts +318 -0
  237. package/src/hooks/useAddressAutocomplete.ts +268 -0
  238. package/src/hooks/useFileDisplay.ts +3 -15
  239. package/src/hooks/useFileReference.test.ts +21 -3
  240. package/src/hooks/useFileReference.ts +3 -24
  241. package/src/hooks/useFileUrlCache.ts +246 -0
  242. package/src/hooks/useInactivityTracker.ts +31 -20
  243. package/src/hooks/useOrganisationSecurity.test.ts +10 -7
  244. package/src/hooks/useOrganisationSecurity.ts +3 -3
  245. package/src/hooks/usePreventTabReload.ts +106 -0
  246. package/src/hooks/useQueryCache.ts +315 -0
  247. package/src/hooks/useSecureDataAccess.ts +2 -2
  248. package/src/index.ts +2 -0
  249. package/src/providers/services/EventServiceProvider.tsx +4 -1
  250. package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
  251. package/src/rbac/api.test.ts +21 -6
  252. package/src/rbac/api.ts +32 -11
  253. package/src/rbac/audit-batched.ts +223 -0
  254. package/src/rbac/audit-enhanced.ts +2 -2
  255. package/src/rbac/audit.test.ts +6 -5
  256. package/src/rbac/audit.ts +34 -6
  257. package/src/rbac/cache-invalidation.ts +63 -12
  258. package/src/rbac/cache.test.ts +2 -2
  259. package/src/rbac/cache.ts +61 -14
  260. package/src/rbac/components/PagePermissionGuard.tsx +19 -10
  261. package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
  262. package/src/rbac/config.ts +9 -0
  263. package/src/rbac/engine.ts +2 -21
  264. package/src/rbac/hooks/usePermissions.ts +21 -5
  265. package/src/rbac/index.ts +19 -0
  266. package/src/rbac/performance.ts +210 -0
  267. package/src/rbac/request-deduplication.ts +87 -0
  268. package/src/rbac/utils/deep-equal.ts +93 -0
  269. package/src/styles/core.css +5 -5
  270. package/src/types/database.generated.ts +63 -9
  271. package/src/types/file-reference.ts +3 -1
  272. package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
  273. package/src/utils/file-reference/index.ts +56 -17
  274. package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
  275. package/src/utils/google-places/googlePlacesUtils.ts +475 -0
  276. package/src/utils/google-places/index.ts +26 -0
  277. package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
  278. package/src/utils/google-places/types.ts +94 -0
  279. package/src/utils/index.ts +23 -0
  280. package/src/utils/request-deduplication.ts +165 -0
  281. package/src/utils/security/secureDataAccess.ts +1 -1
  282. package/src/utils/storage/helpers.ts +211 -4
  283. package/dist/chunk-445GEP27.js.map +0 -1
  284. package/dist/chunk-AISXLWGZ.js.map +0 -1
  285. package/dist/chunk-FMUCXFII.js +0 -76
  286. package/dist/chunk-FMUCXFII.js.map +0 -1
  287. package/dist/chunk-FSFQFJCU.js.map +0 -1
  288. package/dist/chunk-FXFJRTKI.js.map +0 -1
  289. package/dist/chunk-HC67NW5K.js.map +0 -1
  290. package/dist/chunk-IXSNYUCT.js.map +0 -1
  291. package/dist/chunk-MX3EIJGQ.js.map +0 -1
  292. package/dist/chunk-OKI34GZD.js.map +0 -1
  293. package/dist/chunk-STTZQK2I.js.map +0 -1
  294. package/dist/chunk-U6WNSFX5.js.map +0 -1
  295. /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
  296. /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
  297. /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
  298. /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
  299. /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
  300. /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * @file RBAC Role Isolation Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Tests
5
+ * @since 2.0.0
6
+ *
7
+ * Regression tests for RBAC role isolation security fix.
8
+ *
9
+ * These tests verify that organisation roles (e.g., 'leader', 'member') do NOT
10
+ * implicitly grant event-app page permissions. Only the user's actual event-app
11
+ * role (e.g., 'planner', 'event_admin') should determine page permissions.
12
+ *
13
+ * Bug Reference: Organisation role bypasses event-app page permissions
14
+ * Security Impact: HIGH
15
+ */
16
+
17
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
18
+ import { RBACEngine } from '../engine';
19
+ import {
20
+ UUID,
21
+ Permission,
22
+ Scope,
23
+ PermissionCheck
24
+ } from '../types';
25
+ import { rbacCache } from '../cache';
26
+
27
+ // Mock Supabase client
28
+ const createMockSupabaseClient = () => ({
29
+ from: vi.fn(() => ({
30
+ select: vi.fn().mockReturnThis(),
31
+ eq: vi.fn().mockReturnThis(),
32
+ neq: vi.fn().mockReturnThis(),
33
+ in: vi.fn().mockReturnThis(),
34
+ is: vi.fn().mockReturnThis(),
35
+ lte: vi.fn().mockReturnThis(),
36
+ or: vi.fn().mockReturnThis(),
37
+ limit: vi.fn().mockResolvedValue({
38
+ data: [],
39
+ error: null
40
+ }),
41
+ single: vi.fn(),
42
+ maybeSingle: vi.fn(),
43
+ })),
44
+ rpc: vi.fn(),
45
+ });
46
+
47
+ // Test data matching the bug report scenario
48
+ const testData = {
49
+ userId: '00000000-0000-0000-0000-000000000001' as UUID,
50
+ organisationId: '00000000-0000-0000-0000-000000000002' as UUID, // scouts-victoria
51
+ eventId: 'baloo-bistro-event-123',
52
+ appId: '00000000-0000-0000-0000-000000000003' as UUID, // BASE app
53
+ pageId: '00000000-0000-0000-0000-000000000004' as UUID, // configuration page
54
+ pageName: 'configuration'
55
+ };
56
+
57
+ describe('RBAC Role Isolation Tests', () => {
58
+ let engine: RBACEngine;
59
+ let mockSupabase: any;
60
+
61
+ beforeEach(() => {
62
+ mockSupabase = createMockSupabaseClient();
63
+ engine = new RBACEngine(mockSupabase as any);
64
+ rbacCache.clear();
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.clearAllMocks();
69
+ rbacCache.clear();
70
+ });
71
+
72
+ describe('Organisation Role vs Event-App Role Isolation', () => {
73
+ /**
74
+ * Bug Scenario:
75
+ * - User has organisation role: 'leader' (for scouts-victoria)
76
+ * - User has event-app role: 'planner' (for BASE app)
77
+ * - Page permissions: 'event_admin' has full CRUD, 'planner' has only 'read'
78
+ * - User should NOT get 'update' permission just because they are a 'leader'
79
+ */
80
+ it('should deny update permission when user has leader org role but planner event-app role', async () => {
81
+ // Mock: rbac_check_permission_simplified should return FALSE
82
+ // because 'planner' only has 'read' permission, not 'update'
83
+ // The 'leader' org role should NOT grant implicit page permissions
84
+ mockSupabase.rpc.mockResolvedValue({
85
+ data: false,
86
+ error: null
87
+ });
88
+
89
+ const scope: Scope = {
90
+ organisationId: testData.organisationId,
91
+ eventId: testData.eventId,
92
+ appId: testData.appId
93
+ };
94
+
95
+ const permissionCheck: PermissionCheck = {
96
+ userId: testData.userId,
97
+ scope,
98
+ permission: 'update:page.configuration' as Permission,
99
+ pageId: testData.pageId
100
+ };
101
+
102
+ const securityContext = {
103
+ userId: testData.userId,
104
+ organisationId: testData.organisationId,
105
+ timestamp: new Date()
106
+ };
107
+
108
+ const result = await engine.isPermitted(permissionCheck, securityContext);
109
+
110
+ // CRITICAL: Permission must be denied
111
+ expect(result).toBe(false);
112
+
113
+ // Verify the RPC was called with correct parameters
114
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
115
+ 'rbac_check_permission_simplified',
116
+ expect.objectContaining({
117
+ p_user_id: testData.userId,
118
+ p_permission: 'update:page.configuration',
119
+ p_organisation_id: testData.organisationId,
120
+ p_event_id: testData.eventId,
121
+ p_app_id: testData.appId,
122
+ p_page_id: testData.pageId
123
+ })
124
+ );
125
+ });
126
+
127
+ it('should allow read permission when user has planner event-app role', async () => {
128
+ // Mock: rbac_check_permission_simplified should return TRUE
129
+ // because 'planner' has 'read' permission
130
+ mockSupabase.rpc.mockResolvedValue({
131
+ data: true,
132
+ error: null
133
+ });
134
+
135
+ const scope: Scope = {
136
+ organisationId: testData.organisationId,
137
+ eventId: testData.eventId,
138
+ appId: testData.appId
139
+ };
140
+
141
+ const permissionCheck: PermissionCheck = {
142
+ userId: testData.userId,
143
+ scope,
144
+ permission: 'read:page.configuration' as Permission,
145
+ pageId: testData.pageId
146
+ };
147
+
148
+ const securityContext = {
149
+ userId: testData.userId,
150
+ organisationId: testData.organisationId,
151
+ timestamp: new Date()
152
+ };
153
+
154
+ const result = await engine.isPermitted(permissionCheck, securityContext);
155
+
156
+ expect(result).toBe(true);
157
+ });
158
+
159
+ it('should deny delete permission when user has planner event-app role', async () => {
160
+ // 'planner' should NOT have delete permission
161
+ mockSupabase.rpc.mockResolvedValue({
162
+ data: false,
163
+ error: null
164
+ });
165
+
166
+ const scope: Scope = {
167
+ organisationId: testData.organisationId,
168
+ eventId: testData.eventId,
169
+ appId: testData.appId
170
+ };
171
+
172
+ const permissionCheck: PermissionCheck = {
173
+ userId: testData.userId,
174
+ scope,
175
+ permission: 'delete:page.configuration' as Permission,
176
+ pageId: testData.pageId
177
+ };
178
+
179
+ const securityContext = {
180
+ userId: testData.userId,
181
+ organisationId: testData.organisationId,
182
+ timestamp: new Date()
183
+ };
184
+
185
+ const result = await engine.isPermitted(permissionCheck, securityContext);
186
+
187
+ expect(result).toBe(false);
188
+ });
189
+
190
+ it('should deny create permission when user has planner event-app role', async () => {
191
+ // 'planner' should NOT have create permission
192
+ mockSupabase.rpc.mockResolvedValue({
193
+ data: false,
194
+ error: null
195
+ });
196
+
197
+ const scope: Scope = {
198
+ organisationId: testData.organisationId,
199
+ eventId: testData.eventId,
200
+ appId: testData.appId
201
+ };
202
+
203
+ const permissionCheck: PermissionCheck = {
204
+ userId: testData.userId,
205
+ scope,
206
+ permission: 'create:page.configuration' as Permission,
207
+ pageId: testData.pageId
208
+ };
209
+
210
+ const securityContext = {
211
+ userId: testData.userId,
212
+ organisationId: testData.organisationId,
213
+ timestamp: new Date()
214
+ };
215
+
216
+ const result = await engine.isPermitted(permissionCheck, securityContext);
217
+
218
+ expect(result).toBe(false);
219
+ });
220
+ });
221
+
222
+ describe('Event Admin Role Permissions', () => {
223
+ it('should allow full CRUD when user has event_admin role', async () => {
224
+ // event_admin should have all permissions
225
+ mockSupabase.rpc.mockResolvedValue({
226
+ data: true,
227
+ error: null
228
+ });
229
+
230
+ const scope: Scope = {
231
+ organisationId: testData.organisationId,
232
+ eventId: testData.eventId,
233
+ appId: testData.appId
234
+ };
235
+
236
+ const securityContext = {
237
+ userId: testData.userId,
238
+ organisationId: testData.organisationId,
239
+ timestamp: new Date()
240
+ };
241
+
242
+ const operations = ['read', 'create', 'update', 'delete'];
243
+
244
+ for (const operation of operations) {
245
+ const permissionCheck: PermissionCheck = {
246
+ userId: testData.userId,
247
+ scope,
248
+ permission: `${operation}:page.configuration` as Permission,
249
+ pageId: testData.pageId
250
+ };
251
+
252
+ const result = await engine.isPermitted(permissionCheck, securityContext);
253
+ expect(result).toBe(true);
254
+ }
255
+ });
256
+ });
257
+
258
+ describe('Super Admin Bypass', () => {
259
+ it('should allow all permissions for super_admin regardless of event-app role', async () => {
260
+ // Super admin bypasses all checks
261
+ mockSupabase.rpc.mockResolvedValue({
262
+ data: true,
263
+ error: null
264
+ });
265
+
266
+ const scope: Scope = {
267
+ organisationId: testData.organisationId,
268
+ eventId: testData.eventId,
269
+ appId: testData.appId
270
+ };
271
+
272
+ const securityContext = {
273
+ userId: testData.userId,
274
+ organisationId: testData.organisationId,
275
+ timestamp: new Date()
276
+ };
277
+
278
+ const permissionCheck: PermissionCheck = {
279
+ userId: testData.userId,
280
+ scope,
281
+ permission: 'delete:page.configuration' as Permission,
282
+ pageId: testData.pageId
283
+ };
284
+
285
+ const result = await engine.isPermitted(permissionCheck, securityContext);
286
+
287
+ expect(result).toBe(true);
288
+ });
289
+ });
290
+
291
+ describe('Org Admin Bypass', () => {
292
+ it('should allow org_admin to have all permissions within their organisation', async () => {
293
+ // org_admin has all permissions within their org (org-level bypass)
294
+ mockSupabase.rpc.mockResolvedValue({
295
+ data: true,
296
+ error: null
297
+ });
298
+
299
+ const scope: Scope = {
300
+ organisationId: testData.organisationId,
301
+ eventId: testData.eventId,
302
+ appId: testData.appId
303
+ };
304
+
305
+ const securityContext = {
306
+ userId: testData.userId,
307
+ organisationId: testData.organisationId,
308
+ timestamp: new Date()
309
+ };
310
+
311
+ const permissionCheck: PermissionCheck = {
312
+ userId: testData.userId,
313
+ scope,
314
+ permission: 'update:page.configuration' as Permission,
315
+ pageId: testData.pageId
316
+ };
317
+
318
+ const result = await engine.isPermitted(permissionCheck, securityContext);
319
+
320
+ expect(result).toBe(true);
321
+ });
322
+ });
323
+
324
+ describe('Role Isolation Edge Cases', () => {
325
+ it('should not leak permissions from organisation role to event-app context', async () => {
326
+ // Even if user has a high-level org role (like 'leader'), they should NOT
327
+ // get event-app page permissions unless their event-app role grants it
328
+ mockSupabase.rpc.mockResolvedValue({
329
+ data: false,
330
+ error: null
331
+ });
332
+
333
+ const scope: Scope = {
334
+ organisationId: testData.organisationId,
335
+ eventId: testData.eventId,
336
+ appId: testData.appId
337
+ };
338
+
339
+ const securityContext = {
340
+ userId: testData.userId,
341
+ organisationId: testData.organisationId,
342
+ timestamp: new Date()
343
+ };
344
+
345
+ // Test with page name instead of UUID (the bug could manifest differently)
346
+ const permissionCheck: PermissionCheck = {
347
+ userId: testData.userId,
348
+ scope,
349
+ permission: 'update:page.configuration' as Permission,
350
+ pageId: testData.pageName // Using page name
351
+ };
352
+
353
+ const result = await engine.isPermitted(permissionCheck, securityContext);
354
+
355
+ expect(result).toBe(false);
356
+ });
357
+
358
+ it('should deny permission when user has no event-app role at all', async () => {
359
+ // User with org membership but no event-app role should be denied
360
+ mockSupabase.rpc.mockResolvedValue({
361
+ data: false,
362
+ error: null
363
+ });
364
+
365
+ const scope: Scope = {
366
+ organisationId: testData.organisationId,
367
+ eventId: testData.eventId,
368
+ appId: testData.appId
369
+ };
370
+
371
+ const securityContext = {
372
+ userId: testData.userId,
373
+ organisationId: testData.organisationId,
374
+ timestamp: new Date()
375
+ };
376
+
377
+ const permissionCheck: PermissionCheck = {
378
+ userId: testData.userId,
379
+ scope,
380
+ permission: 'read:page.configuration' as Permission,
381
+ pageId: testData.pageId
382
+ };
383
+
384
+ const result = await engine.isPermitted(permissionCheck, securityContext);
385
+
386
+ expect(result).toBe(false);
387
+ });
388
+
389
+ it('should handle mixed permission checks correctly', async () => {
390
+ // Test scenario: user has 'planner' role which grants 'read' but not 'update'
391
+ // First call returns true (read), subsequent calls return false (update, create, delete)
392
+ const rpcResponses = [
393
+ { data: true, error: null }, // read: allowed
394
+ { data: false, error: null }, // update: denied
395
+ { data: false, error: null }, // create: denied
396
+ { data: false, error: null } // delete: denied
397
+ ];
398
+
399
+ let callIndex = 0;
400
+ mockSupabase.rpc.mockImplementation(() => {
401
+ const response = rpcResponses[callIndex];
402
+ callIndex++;
403
+ return Promise.resolve(response);
404
+ });
405
+
406
+ const scope: Scope = {
407
+ organisationId: testData.organisationId,
408
+ eventId: testData.eventId,
409
+ appId: testData.appId
410
+ };
411
+
412
+ const securityContext = {
413
+ userId: testData.userId,
414
+ organisationId: testData.organisationId,
415
+ timestamp: new Date()
416
+ };
417
+
418
+ // Test read permission (should be allowed for planner)
419
+ const readResult = await engine.isPermitted({
420
+ userId: testData.userId,
421
+ scope,
422
+ permission: 'read:page.configuration' as Permission,
423
+ pageId: testData.pageId
424
+ }, securityContext);
425
+ expect(readResult).toBe(true);
426
+
427
+ // Test update permission (should be denied for planner)
428
+ const updateResult = await engine.isPermitted({
429
+ userId: testData.userId,
430
+ scope,
431
+ permission: 'update:page.configuration' as Permission,
432
+ pageId: testData.pageId
433
+ }, securityContext);
434
+ expect(updateResult).toBe(false);
435
+
436
+ // Test create permission (should be denied for planner)
437
+ const createResult = await engine.isPermitted({
438
+ userId: testData.userId,
439
+ scope,
440
+ permission: 'create:page.configuration' as Permission,
441
+ pageId: testData.pageId
442
+ }, securityContext);
443
+ expect(createResult).toBe(false);
444
+
445
+ // Test delete permission (should be denied for planner)
446
+ const deleteResult = await engine.isPermitted({
447
+ userId: testData.userId,
448
+ scope,
449
+ permission: 'delete:page.configuration' as Permission,
450
+ pageId: testData.pageId
451
+ }, securityContext);
452
+ expect(deleteResult).toBe(false);
453
+ });
454
+ });
455
+ });
456
+
@@ -45,6 +45,12 @@ vi.mock('./cache', () => ({
45
45
  }
46
46
  }));
47
47
 
48
+ vi.mock('./request-deduplication', () => ({
49
+ getOrCreateRequest: vi.fn((input, checkFn) => checkFn(input)),
50
+ clearInFlightRequests: vi.fn(),
51
+ getInFlightRequestCount: vi.fn(() => 0),
52
+ }));
53
+
48
54
  vi.mock('./config', () => ({
49
55
  createRBACConfig: vi.fn(),
50
56
  getRBACLogger: vi.fn(() => ({
@@ -120,7 +126,14 @@ describe('RBAC API', () => {
120
126
  process.env.NODE_ENV = originalEnv;
121
127
 
122
128
  expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
123
- expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
129
+ expect(mockCreateAuditManager).toHaveBeenCalledWith(
130
+ mockSupabase,
131
+ true,
132
+ expect.objectContaining({
133
+ batchSize: undefined,
134
+ batchWindow: undefined,
135
+ })
136
+ );
124
137
  expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
125
138
  expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
126
139
  });
@@ -659,7 +672,7 @@ describe('RBAC API', () => {
659
672
  });
660
673
 
661
674
  expect(result).toBe(true);
662
- expect(rbacCache.get).toHaveBeenCalledWith(cacheKey);
675
+ expect(rbacCache.get).toHaveBeenCalledWith(cacheKey, true);
663
676
  expect(mockEngine.isPermitted).not.toHaveBeenCalled();
664
677
  });
665
678
 
@@ -689,10 +702,12 @@ describe('RBAC API', () => {
689
702
  organisationId: 'org-456'
690
703
  })
691
704
  );
692
- expect(rbacCache.set).toHaveBeenCalledWith(
693
- expect.any(String),
694
- true
695
- );
705
+ // Check that cache.set was called - pageId presence makes it a page-level check
706
+ expect(rbacCache.set).toHaveBeenCalled();
707
+ const setCall = rbacCache.set.mock.calls[0];
708
+ expect(setCall[0]).toBeTruthy(); // cache key
709
+ expect(setCall[1]).toBe(true); // result
710
+ // setCall[2] is TTL (optional), setCall[3] is useSessionCache (true for page-level)
696
711
  });
697
712
  });
698
713
 
package/src/rbac/api.ts CHANGED
@@ -27,6 +27,8 @@ import { rbacCache, RBACCache, CACHE_PATTERNS } from './cache';
27
27
  import { createRBACConfig, RBACConfig, getRBACLogger } from './config';
28
28
  import { SecurityContext } from './security';
29
29
  import { createLogger } from '../utils/core/logger';
30
+ import { enablePerformanceMonitoring } from './performance';
31
+ import { getOrCreateRequest } from './request-deduplication';
30
32
 
31
33
  const log = createLogger('RBACAPI');
32
34
 
@@ -72,10 +74,20 @@ export function setupRBAC(supabase: SupabaseClient<Database>, config?: Partial<R
72
74
  // Pass security config to engine
73
75
  globalEngine = createRBACEngine(supabase, securityConfig);
74
76
 
75
- // Setup audit manager
76
- const auditManager = createAuditManager(supabase);
77
+ // Setup audit manager with batching configuration
78
+ const useBatchedAudit = config?.audit?.batched !== false && (config?.performance?.enableBatchedAuditLogging !== false);
79
+ const batchConfig = useBatchedAudit ? {
80
+ batchWindow: config?.audit?.batchWindow,
81
+ batchSize: config?.audit?.batchSize,
82
+ } : undefined;
83
+ const auditManager = createAuditManager(supabase, useBatchedAudit, batchConfig);
77
84
  setGlobalAuditManager(auditManager);
78
85
 
86
+ // Setup performance monitoring if enabled
87
+ if (config?.performance?.enablePerformanceTracking) {
88
+ enablePerformanceMonitoring();
89
+ }
90
+
79
91
  logger.info('RBAC system initialized successfully');
80
92
  }
81
93
 
@@ -195,13 +207,16 @@ export async function isPermitted(input: PermissionCheck): Promise<boolean> {
195
207
  /**
196
208
  * Check if user has a specific permission (cached version)
197
209
  *
210
+ * Uses request deduplication to share in-flight requests across components
211
+ * and checks cache before making new requests. Uses session cache for page-level checks.
212
+ *
198
213
  * @param input - Permission check input
199
214
  * @returns Promise resolving to permission result
200
215
  */
201
216
  export async function isPermittedCached(input: PermissionCheck): Promise<boolean> {
202
217
  const { userId, scope, permission, pageId } = input;
203
218
 
204
- // Check cache first
219
+ // Check cache first (checks both short-term and session cache)
205
220
  const cacheKey = RBACCache.generatePermissionKey({
206
221
  userId,
207
222
  organisationId: scope.organisationId!,
@@ -211,18 +226,24 @@ export async function isPermittedCached(input: PermissionCheck): Promise<boolean
211
226
  pageId,
212
227
  });
213
228
 
214
- const cached = rbacCache.get<boolean>(cacheKey);
229
+ const cached = rbacCache.get<boolean>(cacheKey, true);
215
230
  if (cached !== null) {
216
231
  return cached;
217
232
  }
218
233
 
219
- // Check permission
220
- const result = await isPermitted(input);
221
-
222
- // Cache result
223
- rbacCache.set(cacheKey, result);
224
-
225
- return result;
234
+ // Use request deduplication - if same request is in-flight, share the promise
235
+ return getOrCreateRequest(input, async (checkInput) => {
236
+ // Check permission
237
+ const result = await isPermitted(checkInput);
238
+
239
+ // Determine if this is a page-level check (has pageId or permission contains 'page.')
240
+ const isPageLevelCheck = !!pageId || permission.includes('page.');
241
+
242
+ // Cache result - use session cache for page-level checks
243
+ rbacCache.set(cacheKey, result, undefined, isPageLevelCheck);
244
+
245
+ return result;
246
+ });
226
247
  }
227
248
 
228
249
  /**