@jmruthers/pace-core 0.5.185 → 0.5.186
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/{PublicPageProvider-BABf6JCh.d.ts → PublicPageProvider-DIzEzwKl.d.ts} +4 -2
- package/dist/{chunk-STTZQK2I.js → chunk-DAGICKHT.js} +7 -5
- package/dist/chunk-DAGICKHT.js.map +1 -0
- package/dist/{chunk-AISXLWGZ.js → chunk-GRIQLQ52.js} +2 -2
- package/dist/{chunk-HC67NW5K.js → chunk-HDCUMOOI.js} +125 -47
- package/dist/chunk-HDCUMOOI.js.map +1 -0
- package/dist/{chunk-OKI34GZD.js → chunk-OALXJH4Y.js} +2 -2
- package/dist/{chunk-MX3EIJGQ.js → chunk-TC7D3CR3.js} +86 -7
- package/dist/chunk-TC7D3CR3.js.map +1 -0
- package/dist/{chunk-IXSNYUCT.js → chunk-UQWSHFVX.js} +1 -1
- package/dist/chunk-UQWSHFVX.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +3 -3
- package/dist/{database.generated-CBmg2950.d.ts → database.generated-DI89OQeI.d.ts} +63 -9
- package/dist/{file-reference-BjR39ktt.d.ts → file-reference-PRTSLxKx.d.ts} +3 -0
- package/dist/hooks.d.ts +49 -5
- package/dist/hooks.js +6 -4
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-CvnC3d-e.d.ts → usePublicRouteParams-D71QLlg4.d.ts} +2 -2
- package/dist/utils.d.ts +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 +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- 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/AggregateConfig.md +1 -1
- 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 +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 +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 +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/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 +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- 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 +17 -17
- package/docs/api-reference/components.md +26 -12
- package/docs/implementation-guides/file-reference-system.md +24 -2
- package/docs/implementation-guides/file-upload-storage.md +9 -1
- package/package.json +1 -1
- package/scripts/check-pace-core-compliance.js +512 -0
- 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/hooks/index.ts +3 -0
- package/src/hooks/useFileReference.test.ts +1 -0
- package/src/hooks/usePreventTabReload.ts +106 -0
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/rbac/__tests__/rbac-role-isolation.test.ts +456 -0
- package/src/styles/core.css +5 -5
- package/src/types/database.generated.ts +63 -9
- package/src/types/file-reference.ts +3 -0
- package/src/utils/file-reference/__tests__/file-reference.test.ts +58 -4
- package/src/utils/file-reference/index.ts +12 -2
- package/src/utils/security/secureDataAccess.ts +1 -1
- package/src/utils/storage/helpers.ts +68 -0
- 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-STTZQK2I.js.map +0 -1
- /package/dist/{chunk-AISXLWGZ.js.map → chunk-GRIQLQ52.js.map} +0 -0
- /package/dist/{chunk-OKI34GZD.js.map → chunk-OALXJH4Y.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/styles/core.css
CHANGED
|
@@ -240,14 +240,14 @@
|
|
|
240
240
|
/* Custom utility styles go here */
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
/* Hide spinner arrows on number inputs
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
/* Hide spinner arrows on all number inputs (modern UX convention) */
|
|
244
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
245
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
246
246
|
-webkit-appearance: none;
|
|
247
247
|
margin: 0;
|
|
248
248
|
}
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
|
|
250
|
+
input[type="number"] {
|
|
251
251
|
-moz-appearance: textfield;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
@@ -3320,13 +3320,13 @@ export type Database = {
|
|
|
3320
3320
|
},
|
|
3321
3321
|
]
|
|
3322
3322
|
}
|
|
3323
|
-
|
|
3323
|
+
pace_identification: {
|
|
3324
3324
|
Row: {
|
|
3325
3325
|
created_at: string | null
|
|
3326
3326
|
document_number: string | null
|
|
3327
|
-
document_type: string
|
|
3328
3327
|
expiry_date: string | null
|
|
3329
3328
|
id: string
|
|
3329
|
+
identification_type_id: number | null
|
|
3330
3330
|
issue_city: string | null
|
|
3331
3331
|
issue_country: string | null
|
|
3332
3332
|
issue_date: string | null
|
|
@@ -3339,9 +3339,9 @@ export type Database = {
|
|
|
3339
3339
|
Insert: {
|
|
3340
3340
|
created_at?: string | null
|
|
3341
3341
|
document_number?: string | null
|
|
3342
|
-
document_type: string
|
|
3343
3342
|
expiry_date?: string | null
|
|
3344
3343
|
id?: string
|
|
3344
|
+
identification_type_id?: number | null
|
|
3345
3345
|
issue_city?: string | null
|
|
3346
3346
|
issue_country?: string | null
|
|
3347
3347
|
issue_date?: string | null
|
|
@@ -3354,9 +3354,9 @@ export type Database = {
|
|
|
3354
3354
|
Update: {
|
|
3355
3355
|
created_at?: string | null
|
|
3356
3356
|
document_number?: string | null
|
|
3357
|
-
document_type?: string
|
|
3358
3357
|
expiry_date?: string | null
|
|
3359
3358
|
id?: string
|
|
3359
|
+
identification_type_id?: number | null
|
|
3360
3360
|
issue_city?: string | null
|
|
3361
3361
|
issue_country?: string | null
|
|
3362
3362
|
issue_date?: string | null
|
|
@@ -3368,14 +3368,21 @@ export type Database = {
|
|
|
3368
3368
|
}
|
|
3369
3369
|
Relationships: [
|
|
3370
3370
|
{
|
|
3371
|
-
foreignKeyName: "
|
|
3371
|
+
foreignKeyName: "fk_pace_identification_organisation_id"
|
|
3372
3372
|
columns: ["organisation_id"]
|
|
3373
3373
|
isOneToOne: false
|
|
3374
3374
|
referencedRelation: "organisations"
|
|
3375
3375
|
referencedColumns: ["id"]
|
|
3376
3376
|
},
|
|
3377
3377
|
{
|
|
3378
|
-
foreignKeyName: "
|
|
3378
|
+
foreignKeyName: "fk_pace_identification_type_id"
|
|
3379
|
+
columns: ["identification_type_id"]
|
|
3380
|
+
isOneToOne: false
|
|
3381
|
+
referencedRelation: "pace_identification_type"
|
|
3382
|
+
referencedColumns: ["id"]
|
|
3383
|
+
},
|
|
3384
|
+
{
|
|
3385
|
+
foreignKeyName: "pace_identification_member_id_fkey"
|
|
3379
3386
|
columns: ["member_id"]
|
|
3380
3387
|
isOneToOne: false
|
|
3381
3388
|
referencedRelation: "pace_member"
|
|
@@ -3383,6 +3390,53 @@ export type Database = {
|
|
|
3383
3390
|
},
|
|
3384
3391
|
]
|
|
3385
3392
|
}
|
|
3393
|
+
pace_identification_type: {
|
|
3394
|
+
Row: {
|
|
3395
|
+
created_at: string | null
|
|
3396
|
+
created_by: string | null
|
|
3397
|
+
description: string | null
|
|
3398
|
+
id: number
|
|
3399
|
+
is_active: boolean | null
|
|
3400
|
+
name: string
|
|
3401
|
+
organisation_id: string
|
|
3402
|
+
sort_order: number | null
|
|
3403
|
+
updated_at: string | null
|
|
3404
|
+
updated_by: string | null
|
|
3405
|
+
}
|
|
3406
|
+
Insert: {
|
|
3407
|
+
created_at?: string | null
|
|
3408
|
+
created_by?: string | null
|
|
3409
|
+
description?: string | null
|
|
3410
|
+
id?: never
|
|
3411
|
+
is_active?: boolean | null
|
|
3412
|
+
name: string
|
|
3413
|
+
organisation_id: string
|
|
3414
|
+
sort_order?: number | null
|
|
3415
|
+
updated_at?: string | null
|
|
3416
|
+
updated_by?: string | null
|
|
3417
|
+
}
|
|
3418
|
+
Update: {
|
|
3419
|
+
created_at?: string | null
|
|
3420
|
+
created_by?: string | null
|
|
3421
|
+
description?: string | null
|
|
3422
|
+
id?: never
|
|
3423
|
+
is_active?: boolean | null
|
|
3424
|
+
name?: string
|
|
3425
|
+
organisation_id?: string
|
|
3426
|
+
sort_order?: number | null
|
|
3427
|
+
updated_at?: string | null
|
|
3428
|
+
updated_by?: string | null
|
|
3429
|
+
}
|
|
3430
|
+
Relationships: [
|
|
3431
|
+
{
|
|
3432
|
+
foreignKeyName: "pace_identification_type_organisation_id_fkey"
|
|
3433
|
+
columns: ["organisation_id"]
|
|
3434
|
+
isOneToOne: false
|
|
3435
|
+
referencedRelation: "organisations"
|
|
3436
|
+
referencedColumns: ["id"]
|
|
3437
|
+
},
|
|
3438
|
+
]
|
|
3439
|
+
}
|
|
3386
3440
|
pace_member: {
|
|
3387
3441
|
Row: {
|
|
3388
3442
|
address_id: string | null
|
|
@@ -3858,7 +3912,7 @@ export type Database = {
|
|
|
3858
3912
|
},
|
|
3859
3913
|
]
|
|
3860
3914
|
}
|
|
3861
|
-
|
|
3915
|
+
pace_qualification: {
|
|
3862
3916
|
Row: {
|
|
3863
3917
|
created_at: string | null
|
|
3864
3918
|
credential_id: string | null
|
|
@@ -3900,14 +3954,14 @@ export type Database = {
|
|
|
3900
3954
|
}
|
|
3901
3955
|
Relationships: [
|
|
3902
3956
|
{
|
|
3903
|
-
foreignKeyName: "
|
|
3957
|
+
foreignKeyName: "fk_pace_qualification_organisation_id"
|
|
3904
3958
|
columns: ["organisation_id"]
|
|
3905
3959
|
isOneToOne: false
|
|
3906
3960
|
referencedRelation: "organisations"
|
|
3907
3961
|
referencedColumns: ["id"]
|
|
3908
3962
|
},
|
|
3909
3963
|
{
|
|
3910
|
-
foreignKeyName: "
|
|
3964
|
+
foreignKeyName: "pace_qualification_member_id_fkey"
|
|
3911
3965
|
columns: ["member_id"]
|
|
3912
3966
|
isOneToOne: false
|
|
3913
3967
|
referencedRelation: "pace_member"
|
|
@@ -55,6 +55,7 @@ export enum FileCategory {
|
|
|
55
55
|
* Options for uploading a file with a file reference
|
|
56
56
|
* @property pageContext - The page context where the file upload occurs (e.g., 'configuration', 'forms', 'applications')
|
|
57
57
|
* Used for context-aware permission checks. Required to check appropriate page-level permissions.
|
|
58
|
+
* @property event_id - Optional event ID for event-scoped permission checks. Required for event-based apps.
|
|
58
59
|
*/
|
|
59
60
|
export interface FileUploadOptions {
|
|
60
61
|
table_name: string;
|
|
@@ -62,7 +63,9 @@ export interface FileUploadOptions {
|
|
|
62
63
|
organisation_id: string;
|
|
63
64
|
app_id: AppId;
|
|
64
65
|
category: FileCategory;
|
|
66
|
+
folder: string; // Folder name in storage bucket (e.g., 'profile_photos', 'documents')
|
|
65
67
|
pageContext: string;
|
|
68
|
+
event_id?: string;
|
|
66
69
|
is_public?: boolean;
|
|
67
70
|
custom_metadata?: Record<string, unknown>;
|
|
68
71
|
}
|