@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.
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DrLDztHt.d.ts} +214 -107
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OKI34GZD.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-MX3EIJGQ.js → chunk-C4OYJOV4.js} +631 -97
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HC67NW5K.js → chunk-LBBUPSSC.js} +863 -552
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-IXSNYUCT.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/chunk-SAUPYVLF.js.map +1 -0
- package/dist/{chunk-AISXLWGZ.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-STTZQK2I.js → chunk-ULX5FYEM.js} +9 -7
- package/dist/chunk-ULX5FYEM.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/{file-reference-BjR39ktt.d.ts → file-reference-D037xOFK.d.ts} +3 -1
- package/dist/hooks.d.ts +265 -6
- package/dist/hooks.js +148 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +25 -10
- package/dist/index.js +65 -30
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +125 -8
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +3 -3
- package/dist/utils.d.ts +214 -4
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +6 -6
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.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/ComplianceResult.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/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- 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 +33 -9
- package/docs/api/interfaces/FileUploadProps.md +36 -14
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.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/LoggerConfig.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 +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +6 -6
- 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/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +27 -4
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.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/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.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 +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.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 +328 -69
- package/docs/api-reference/components.md +26 -12
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-reference-system.md +24 -2
- package/docs/implementation-guides/file-upload-storage.md +38 -1
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.js +512 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/FileUpload/FileUpload.test.tsx +2 -0
- package/src/components/FileUpload/FileUpload.tsx +7 -1
- package/src/components/Header/Header.tsx +2 -5
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +134 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +9 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +21 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +3 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +89 -8
- package/src/utils/file-reference/index.ts +56 -17
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +211 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-AISXLWGZ.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-HC67NW5K.js.map +0 -1
- package/dist/chunk-IXSNYUCT.js.map +0 -1
- package/dist/chunk-MX3EIJGQ.js.map +0 -1
- package/dist/chunk-OKI34GZD.js.map +0 -1
- package/dist/chunk-STTZQK2I.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /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
|
+
|
package/src/rbac/api.test.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
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
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
/**
|