@jmruthers/pace-core 0.5.187 → 0.5.189
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-K3RJRSOX.js → DataTable-GUFUNZ3N.js} +5 -5
- package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-B8HaLe69.d.ts} +47 -17
- package/dist/{UnifiedAuthProvider-B76OWOAT.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
- package/dist/{chunk-FMTK4XNN.js → chunk-2UUZZJFT.js} +3 -3
- package/dist/{chunk-3IC5WCMO.js → chunk-3GOZZZYH.js} +3 -3
- package/dist/{chunk-ULX5FYEM.js → chunk-DDM4CCYT.js} +3 -3
- package/dist/{chunk-K2JGDXGU.js → chunk-E7UAOUMY.js} +2 -2
- package/dist/{chunk-T6ZJVI3A.js → chunk-IM4QE42D.js} +4 -4
- package/dist/{chunk-3NFNJOO7.js → chunk-MX64ZF6I.js} +4 -4
- package/dist/{chunk-C4OYJOV4.js → chunk-UCQSRW7Z.js} +829 -829
- package/dist/chunk-UCQSRW7Z.js.map +1 -0
- package/dist/{chunk-WK2Y6TGA.js → chunk-VGZZXKBR.js} +3 -3
- package/dist/chunk-VGZZXKBR.js.map +1 -0
- package/dist/{chunk-LBBUPSSC.js → chunk-YGPFYGA6.js} +3760 -3692
- package/dist/chunk-YGPFYGA6.js.map +1 -0
- package/dist/components.d.ts +1 -2
- package/dist/components.js +6 -10
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +5 -5
- package/dist/index.d.ts +1 -2
- package/dist/index.js +9 -13
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.js +5 -5
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +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 +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/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 +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +128 -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 +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/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/ParsedAddress.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 +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/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 +1 -1
- 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/RBACPerformanceMetrics.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 +1 -1
- 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 +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/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 +6 -45
- package/docs/api-reference/components.md +57 -22
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/implementation-guides/public-pages.md +140 -1230
- package/docs/standards/05-security-standard.md +3 -1
- package/docs/standards/07-rbac-and-rls-standard.md +356 -0
- package/package.json +1 -2
- package/src/__tests__/public-recipe-view.test.ts +199 -0
- package/src/__tests__/rls-policies.test.ts +333 -0
- package/src/components/Avatar/Avatar.test.tsx +183 -209
- package/src/components/Avatar/Avatar.tsx +179 -53
- package/src/components/Avatar/index.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +7 -9
- package/src/components/UserMenu/UserMenu.tsx +7 -5
- package/src/components/index.ts +2 -1
- package/src/index.ts +2 -1
- package/src/services/OrganisationService.ts +5 -4
- package/dist/chunk-C4OYJOV4.js.map +0 -1
- package/dist/chunk-LBBUPSSC.js.map +0 -1
- package/dist/chunk-WK2Y6TGA.js.map +0 -1
- /package/dist/{DataTable-K3RJRSOX.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-B76OWOAT.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
- /package/dist/{chunk-FMTK4XNN.js.map → chunk-2UUZZJFT.js.map} +0 -0
- /package/dist/{chunk-3IC5WCMO.js.map → chunk-3GOZZZYH.js.map} +0 -0
- /package/dist/{chunk-ULX5FYEM.js.map → chunk-DDM4CCYT.js.map} +0 -0
- /package/dist/{chunk-K2JGDXGU.js.map → chunk-E7UAOUMY.js.map} +0 -0
- /package/dist/{chunk-T6ZJVI3A.js.map → chunk-IM4QE42D.js.map} +0 -0
- /package/dist/{chunk-3NFNJOO7.js.map → chunk-MX64ZF6I.js.map} +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## RLS Policy Performance Rules
|
|
17
17
|
- **NEVER use subqueries in RLS policies** - They execute for every row check and cause severe performance degradation
|
|
18
|
-
- **ALWAYS use helper functions** - Create STABLE SECURITY DEFINER functions for lookups (e.g., `
|
|
18
|
+
- **ALWAYS use helper functions** - Create STABLE SECURITY DEFINER functions for lookups (e.g., `get_app_id()`, `get_form_event_id()`)
|
|
19
19
|
- **Helper functions must be**:
|
|
20
20
|
- `STABLE` - Results are consistent within a transaction
|
|
21
21
|
- `SECURITY DEFINER` - Bypass RLS to avoid recursion
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
- **Test RLS policies** - Verify they don't cause query timeouts or N+1 query patterns
|
|
24
24
|
- **Monitor performance** - Watch for slow queries after RLS policy changes
|
|
25
25
|
|
|
26
|
+
**See [RBAC and RLS Standard](./07-rbac-and-rls-standard.md) for detailed RLS policy patterns and helper function documentation.**
|
|
27
|
+
|
|
26
28
|
## Logging Rules
|
|
27
29
|
Allowed:
|
|
28
30
|
- IDs
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
---
|
|
2
|
+
lastUpdated: 2025-01-28T00:00:00+11:00
|
|
3
|
+
version: 0.5.181
|
|
4
|
+
reviewedBy: database-performance-audit
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# RBAC and RLS Standard
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Define standards for Row-Level Security (RLS) policies and Role-Based Access Control (RBAC) integration to ensure security, performance, and maintainability.
|
|
12
|
+
|
|
13
|
+
## Principles
|
|
14
|
+
|
|
15
|
+
- **Performance First**: All RLS policies must use optimized helper functions
|
|
16
|
+
- **Security by Default**: Deny access unless explicitly allowed
|
|
17
|
+
- **Consistent Patterns**: Use standardized helper functions across all policies
|
|
18
|
+
- **Performance Monitoring**: Regular validation of query performance
|
|
19
|
+
|
|
20
|
+
## RLS Policy Performance Requirements
|
|
21
|
+
|
|
22
|
+
### Helper Function Requirements
|
|
23
|
+
|
|
24
|
+
All helper functions used in RLS policies **MUST** have these attributes:
|
|
25
|
+
|
|
26
|
+
| Requirement | Why | Example |
|
|
27
|
+
|------------|-----|---------|
|
|
28
|
+
| `STABLE` | Prevents re-evaluation for each row | `STABLE` |
|
|
29
|
+
| `SECURITY DEFINER` | Bypass RLS to avoid recursion | `SECURITY DEFINER` |
|
|
30
|
+
| `SET search_path TO 'public'` | Prevent search path injection | `SET search_path TO 'public'` |
|
|
31
|
+
| No inline `auth.uid()` | Causes InitPlan nodes, severe performance degradation | Use helper function instead |
|
|
32
|
+
|
|
33
|
+
### Helper Function Template
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
CREATE OR REPLACE FUNCTION function_name(parameters)
|
|
37
|
+
RETURNS return_type
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
STABLE -- ✅ Required
|
|
40
|
+
SECURITY DEFINER -- ✅ Required
|
|
41
|
+
SET search_path TO public -- ✅ Required
|
|
42
|
+
AS $$
|
|
43
|
+
DECLARE
|
|
44
|
+
-- Declarations
|
|
45
|
+
BEGIN
|
|
46
|
+
-- Function body
|
|
47
|
+
RETURN result;
|
|
48
|
+
END;
|
|
49
|
+
$$;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Forbidden Patterns
|
|
53
|
+
|
|
54
|
+
**❌ NEVER use these patterns in RLS policies:**
|
|
55
|
+
|
|
56
|
+
```sql
|
|
57
|
+
-- ❌ BAD: Inline auth.uid() call
|
|
58
|
+
CREATE POLICY "bad_policy" ON table_name
|
|
59
|
+
FOR SELECT USING (
|
|
60
|
+
user_id = auth.uid() -- Called for every row!
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
-- ❌ BAD: Subquery in policy
|
|
64
|
+
CREATE POLICY "bad_policy" ON table_name
|
|
65
|
+
FOR SELECT USING (
|
|
66
|
+
organisation_id IN (
|
|
67
|
+
SELECT organisation_id FROM rbac_organisation_roles
|
|
68
|
+
WHERE user_id = auth.uid() -- Executes for every row!
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- ❌ BAD: current_setting in policy
|
|
73
|
+
CREATE POLICY "bad_policy" ON table_name
|
|
74
|
+
FOR SELECT USING (
|
|
75
|
+
organisation_id = current_setting('app.organisation_id')::UUID -- Called for every row!
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Required Patterns
|
|
80
|
+
|
|
81
|
+
**✅ ALWAYS use these patterns:**
|
|
82
|
+
|
|
83
|
+
```sql
|
|
84
|
+
-- ✅ GOOD: Use helper function
|
|
85
|
+
CREATE POLICY "good_policy" ON table_name
|
|
86
|
+
FOR SELECT USING (
|
|
87
|
+
check_user_organisation_access(organisation_id) -- Single function call per row
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- ✅ GOOD: Use helper function for app ID
|
|
91
|
+
CREATE POLICY "good_policy" ON table_name
|
|
92
|
+
FOR SELECT USING (
|
|
93
|
+
rbac_check_permission_simplified(
|
|
94
|
+
auth.uid(),
|
|
95
|
+
'read:page.table_name',
|
|
96
|
+
organisation_id,
|
|
97
|
+
event_id,
|
|
98
|
+
get_app_id('APP_NAME'), -- ✅ Helper function
|
|
99
|
+
'table_name'
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Standard Helper Functions
|
|
105
|
+
|
|
106
|
+
### Core Helper Functions
|
|
107
|
+
|
|
108
|
+
These functions are available for use in RLS policies:
|
|
109
|
+
|
|
110
|
+
#### `is_super_admin()`
|
|
111
|
+
- **Returns**: `boolean`
|
|
112
|
+
- **Purpose**: Checks if current user is a super admin
|
|
113
|
+
- **Usage**: `is_super_admin()`
|
|
114
|
+
|
|
115
|
+
#### `check_user_organisation_access(p_organisation_id UUID)`
|
|
116
|
+
- **Returns**: `boolean`
|
|
117
|
+
- **Purpose**: Checks if current user has access to the specified organisation
|
|
118
|
+
- **Usage**: `check_user_organisation_access(organisation_id)`
|
|
119
|
+
|
|
120
|
+
#### `check_user_event_access(p_event_id TEXT)`
|
|
121
|
+
- **Returns**: `boolean`
|
|
122
|
+
- **Purpose**: Checks if current user has access to the specified event
|
|
123
|
+
- **Usage**: `check_user_event_access(event_id)`
|
|
124
|
+
|
|
125
|
+
#### `check_public_event_access(p_event_id TEXT)`
|
|
126
|
+
- **Returns**: `boolean`
|
|
127
|
+
- **Purpose**: Checks if the specified event is publicly accessible
|
|
128
|
+
- **Usage**: `check_public_event_access(event_id)`
|
|
129
|
+
|
|
130
|
+
#### `get_app_id(p_app_name TEXT)`
|
|
131
|
+
- **Returns**: `UUID`
|
|
132
|
+
- **Purpose**: Returns the UUID for any registered app
|
|
133
|
+
- **Usage**: `get_app_id('BASE')`, `get_app_id('PACE')`, `get_app_id('CAKE')`
|
|
134
|
+
- **Note**: Replaces `get_base_app_id()`, `get_pace_app_id()`, `util_get_cake_app_id()`
|
|
135
|
+
|
|
136
|
+
#### `get_organisation_context()`
|
|
137
|
+
- **Returns**: `UUID`
|
|
138
|
+
- **Purpose**: Returns the current organisation context from the session
|
|
139
|
+
- **Usage**: `get_organisation_context()`
|
|
140
|
+
|
|
141
|
+
### Event Helper Functions
|
|
142
|
+
|
|
143
|
+
#### `get_unit_event_id(p_unit_id TEXT)`
|
|
144
|
+
- **Returns**: `TEXT`
|
|
145
|
+
- **Purpose**: Returns the event_id for a given unit_id
|
|
146
|
+
- **Usage**: `get_unit_event_id(unit_id)`
|
|
147
|
+
|
|
148
|
+
#### `get_form_event_id(p_form_id UUID)`
|
|
149
|
+
- **Returns**: `TEXT`
|
|
150
|
+
- **Purpose**: Returns the event_id for a given form_id
|
|
151
|
+
- **Usage**: `get_form_event_id(form_id)`
|
|
152
|
+
|
|
153
|
+
#### `get_form_response_event_id(p_response_id UUID)`
|
|
154
|
+
- **Returns**: `TEXT`
|
|
155
|
+
- **Purpose**: Returns the event_id for a given form_response_id
|
|
156
|
+
- **Usage**: `get_form_response_event_id(response_id)`
|
|
157
|
+
|
|
158
|
+
#### `get_event_organisation_id(p_event_id TEXT)`
|
|
159
|
+
- **Returns**: `UUID`
|
|
160
|
+
- **Purpose**: Returns the organisation_id for a given event_id
|
|
161
|
+
- **Usage**: `get_event_organisation_id(event_id)`
|
|
162
|
+
|
|
163
|
+
### RBAC Permission Helper Functions
|
|
164
|
+
|
|
165
|
+
#### `check_rbac_permission_with_context(p_permission TEXT, p_page_name TEXT, p_organisation_id UUID, p_event_id TEXT, p_app_id UUID)`
|
|
166
|
+
- **Returns**: `boolean`
|
|
167
|
+
- **Purpose**: STABLE SECURITY DEFINER wrapper for `rbac_check_permission_simplified()`. Use this in RLS policies instead of calling `rbac_check_permission_simplified()` with `auth.uid()` directly.
|
|
168
|
+
- **Usage**: `check_rbac_permission_with_context('read:page.table_name', 'table_name', organisation_id, NULL, get_app_id('APP_NAME'))`
|
|
169
|
+
- **Note**: This wrapper ensures `auth.uid()` is called once per function invocation (not per row) and the function is STABLE for RLS policy performance.
|
|
170
|
+
|
|
171
|
+
## RLS Policy Patterns
|
|
172
|
+
|
|
173
|
+
### Standard Organisation-Scoped Policy
|
|
174
|
+
|
|
175
|
+
```sql
|
|
176
|
+
CREATE POLICY "rbac_select_table_name" ON table_name
|
|
177
|
+
FOR SELECT TO authenticated
|
|
178
|
+
USING (
|
|
179
|
+
organisation_id IS NOT NULL
|
|
180
|
+
AND (
|
|
181
|
+
is_super_admin()
|
|
182
|
+
OR check_user_organisation_access(organisation_id)
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Standard Event-Scoped Policy
|
|
188
|
+
|
|
189
|
+
```sql
|
|
190
|
+
CREATE POLICY "rbac_select_table_name" ON table_name
|
|
191
|
+
FOR SELECT TO authenticated
|
|
192
|
+
USING (
|
|
193
|
+
organisation_id IS NOT NULL
|
|
194
|
+
AND (
|
|
195
|
+
is_super_admin()
|
|
196
|
+
OR check_user_event_access(event_id)
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### RBAC Permission-Based Policy
|
|
202
|
+
|
|
203
|
+
```sql
|
|
204
|
+
CREATE POLICY "rbac_select_table_name" ON table_name
|
|
205
|
+
FOR SELECT TO authenticated
|
|
206
|
+
USING (
|
|
207
|
+
organisation_id IS NOT NULL
|
|
208
|
+
AND (
|
|
209
|
+
is_super_admin()
|
|
210
|
+
OR check_rbac_permission_with_context(
|
|
211
|
+
'read:page.table_name',
|
|
212
|
+
'table_name',
|
|
213
|
+
organisation_id,
|
|
214
|
+
event_id,
|
|
215
|
+
get_app_id('APP_NAME')
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Note**: Always use `check_rbac_permission_with_context()` instead of calling `rbac_check_permission_simplified()` with `auth.uid()` directly. The wrapper function is STABLE SECURITY DEFINER and ensures optimal performance.
|
|
222
|
+
|
|
223
|
+
### Public Access Policy
|
|
224
|
+
|
|
225
|
+
```sql
|
|
226
|
+
CREATE POLICY "public_select_table_name" ON table_name
|
|
227
|
+
FOR SELECT TO anon
|
|
228
|
+
USING (
|
|
229
|
+
check_public_event_access(event_id)
|
|
230
|
+
);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## App Ownership
|
|
234
|
+
|
|
235
|
+
Tables are assigned to specific apps for RBAC permission checking:
|
|
236
|
+
|
|
237
|
+
| App | Tables |
|
|
238
|
+
|-----|--------|
|
|
239
|
+
| **BASE** | `base_application`, `base_questions` |
|
|
240
|
+
| **PACE** | `pace_*` tables, `form_*` tables |
|
|
241
|
+
| **CAKE** | `cake_*` tables |
|
|
242
|
+
| **TRAC** | `trac_*` tables |
|
|
243
|
+
| **MEDI** | `medi_*` tables |
|
|
244
|
+
| **MINT** | `mint_*` tables (handled separately) |
|
|
245
|
+
| **PORTAL** | `form_context_types` (shared with PACE) |
|
|
246
|
+
|
|
247
|
+
## Testing Requirements
|
|
248
|
+
|
|
249
|
+
### Before Merging RLS Changes
|
|
250
|
+
|
|
251
|
+
1. **Run RLS Compliance Audit**:
|
|
252
|
+
```bash
|
|
253
|
+
npm run audit:rls
|
|
254
|
+
```
|
|
255
|
+
This checks for violations and should pass with zero violations.
|
|
256
|
+
|
|
257
|
+
2. **Run Supabase Advisors**:
|
|
258
|
+
```bash
|
|
259
|
+
supabase advisors performance
|
|
260
|
+
supabase advisors security
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
3. **Run Database Tests**:
|
|
264
|
+
```bash
|
|
265
|
+
timeout 120 npm run test:db
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
4. **Run Application Tests**:
|
|
269
|
+
```bash
|
|
270
|
+
timeout 60 npm run test
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
5. **Verify Performance**:
|
|
274
|
+
- Use EXPLAIN ANALYZE to verify no InitPlan nodes
|
|
275
|
+
- Verify queries complete in < 1 second
|
|
276
|
+
- Check Supabase Advisors show zero `auth_rls_initplan` warnings
|
|
277
|
+
|
|
278
|
+
### Test Coverage Requirements
|
|
279
|
+
|
|
280
|
+
- [ ] Policy coverage: All tables (except mint_*) have RLS enabled and policies
|
|
281
|
+
- [ ] Performance: Queries complete in < 1 second
|
|
282
|
+
- [ ] Security: Cross-organisation access is blocked
|
|
283
|
+
- [ ] Helper functions: All are STABLE SECURITY DEFINER
|
|
284
|
+
|
|
285
|
+
## Maintenance
|
|
286
|
+
|
|
287
|
+
### Ongoing RLS Compliance Monitoring
|
|
288
|
+
|
|
289
|
+
**Run RLS audit regularly** to catch any rogue policies that violate standards:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Quick audit (recommended before merging PRs)
|
|
293
|
+
npm run audit:rls
|
|
294
|
+
|
|
295
|
+
# Or use test database
|
|
296
|
+
npm run audit:rls:test
|
|
297
|
+
|
|
298
|
+
# For CI/CD (fails on violations)
|
|
299
|
+
npm run audit:rls:ci
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The audit checks for:
|
|
303
|
+
- Tables with RLS enabled but no policies
|
|
304
|
+
- Policies using inline `auth.uid()` calls
|
|
305
|
+
- Policies using `current_setting()` directly
|
|
306
|
+
- Helper functions missing STABLE/SECURITY DEFINER
|
|
307
|
+
|
|
308
|
+
### Weekly Health Checks
|
|
309
|
+
|
|
310
|
+
Run weekly:
|
|
311
|
+
```bash
|
|
312
|
+
# RLS compliance audit
|
|
313
|
+
npm run audit:rls
|
|
314
|
+
|
|
315
|
+
# Performance advisors
|
|
316
|
+
supabase advisors performance
|
|
317
|
+
|
|
318
|
+
# Security advisors
|
|
319
|
+
supabase advisors security
|
|
320
|
+
|
|
321
|
+
# Review slow queries
|
|
322
|
+
# (configure log_min_duration_statement = 1000)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Monitoring
|
|
326
|
+
|
|
327
|
+
Monitor:
|
|
328
|
+
- Query duration > 5 seconds
|
|
329
|
+
- Connection pool exhaustion (>80% utilization)
|
|
330
|
+
- CPU usage > 80%
|
|
331
|
+
- Memory usage > 80%
|
|
332
|
+
- `auth_rls_initplan` advisor warnings > 0
|
|
333
|
+
|
|
334
|
+
## Migration Guidelines
|
|
335
|
+
|
|
336
|
+
### Creating New RLS Policies
|
|
337
|
+
|
|
338
|
+
1. **Use helper functions** - Never inline `auth.uid()` or `current_setting()`
|
|
339
|
+
2. **Test with EXPLAIN ANALYZE** - Verify no InitPlan nodes
|
|
340
|
+
3. **Load test** - Test with realistic data volumes
|
|
341
|
+
4. **Run Supabase Advisors** - Verify no new warnings
|
|
342
|
+
|
|
343
|
+
### Updating Existing Policies
|
|
344
|
+
|
|
345
|
+
1. **Backup existing policies** before changes
|
|
346
|
+
2. **Test in development** first
|
|
347
|
+
3. **Monitor performance** after deployment
|
|
348
|
+
4. **Rollback plan** ready if issues occur
|
|
349
|
+
|
|
350
|
+
## Related Documentation
|
|
351
|
+
|
|
352
|
+
- [Security Standard](./05-security-standard.md)
|
|
353
|
+
- [RLS Policy Remediation Plan](../troubleshooting/rls-policy-remediation-plan-combined.md)
|
|
354
|
+
- [Database Unhealthiness Diagnosis](../troubleshooting/database-unhealthiness-diagnosis.md)
|
|
355
|
+
- [RBAC-RLS Integration Guide](../rbac/rbac-rls-integration.md)
|
|
356
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmruthers/pace-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.189",
|
|
4
4
|
"description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -197,7 +197,6 @@
|
|
|
197
197
|
"author": "PACE Team",
|
|
198
198
|
"license": "MIT",
|
|
199
199
|
"peerDependencies": {
|
|
200
|
-
"@radix-ui/react-avatar": "^1.0.0",
|
|
201
200
|
"@radix-ui/react-checkbox": "^1.0.0",
|
|
202
201
|
"@radix-ui/react-dialog": "^1.0.0",
|
|
203
202
|
"@radix-ui/react-label": "^2.0.0",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Public Recipe View Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RLS/Tests/PublicViews
|
|
5
|
+
* @since 0.5.181
|
|
6
|
+
*
|
|
7
|
+
* Regression tests for the public_recipe_details view to verify:
|
|
8
|
+
* - Anonymous access works only when event.public_readable = true
|
|
9
|
+
* - View exposes only expected columns
|
|
10
|
+
* - Respects diet/meal filters
|
|
11
|
+
* - Performance is acceptable (< 500ms)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
15
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
16
|
+
import type { Database } from '../../types/database';
|
|
17
|
+
|
|
18
|
+
const TEST_TIMEOUT = 5000;
|
|
19
|
+
const PERFORMANCE_THRESHOLD = 500; // 500ms for public view queries
|
|
20
|
+
|
|
21
|
+
// Check if we're using real test-db (via environment variables)
|
|
22
|
+
const USE_REAL_DB = !!(process.env.SUPABASE_URL && process.env.VITE_SUPABASE_ANON_KEY);
|
|
23
|
+
const TEST_SUPABASE_URL = process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL;
|
|
24
|
+
const TEST_SUPABASE_ANON_KEY = process.env.TEST_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
|
|
25
|
+
const TEST_SUPABASE_SERVICE_ROLE_KEY = process.env.TEST_SUPABASE_SERVICE_ROLE_KEY;
|
|
26
|
+
|
|
27
|
+
let anonClient: SupabaseClient<Database>;
|
|
28
|
+
let authenticatedClient: SupabaseClient<Database>;
|
|
29
|
+
|
|
30
|
+
const publicEvent = {
|
|
31
|
+
event_id: 'public-event-1',
|
|
32
|
+
event_name: 'Public Event',
|
|
33
|
+
public_readable: true,
|
|
34
|
+
is_visible: true,
|
|
35
|
+
organisation_id: 'org-1' as any
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const privateEvent = {
|
|
39
|
+
event_id: 'private-event-1',
|
|
40
|
+
event_name: 'Private Event',
|
|
41
|
+
public_readable: false,
|
|
42
|
+
is_visible: true,
|
|
43
|
+
organisation_id: 'org-1' as any
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_ANON_KEY)('Public Recipe View - Anonymous Access', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
if (USE_REAL_DB && TEST_SUPABASE_URL && TEST_SUPABASE_ANON_KEY) {
|
|
49
|
+
anonClient = createClient<Database>(TEST_SUPABASE_URL, TEST_SUPABASE_ANON_KEY);
|
|
50
|
+
authenticatedClient = createClient<Database>(TEST_SUPABASE_URL, TEST_SUPABASE_SERVICE_ROLE_KEY || TEST_SUPABASE_ANON_KEY);
|
|
51
|
+
} else {
|
|
52
|
+
// This should not happen due to skipIf, but provide fallback
|
|
53
|
+
throw new Error('Test database credentials not available. Set SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables.');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Public Event Access', () => {
|
|
58
|
+
it('should allow anonymous access when event.public_readable = true', async () => {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
const { data, error } = await anonClient
|
|
61
|
+
.from('public_recipe_details')
|
|
62
|
+
.select('*')
|
|
63
|
+
.eq('event_id', publicEvent.event_id);
|
|
64
|
+
const duration = Date.now() - start;
|
|
65
|
+
|
|
66
|
+
expect(error).toBeNull();
|
|
67
|
+
expect(data).toBeDefined();
|
|
68
|
+
expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
|
|
69
|
+
}, TEST_TIMEOUT);
|
|
70
|
+
|
|
71
|
+
it('should block anonymous access when event.public_readable = false', async () => {
|
|
72
|
+
const { data, error } = await anonClient
|
|
73
|
+
.from('public_recipe_details')
|
|
74
|
+
.select('*')
|
|
75
|
+
.eq('event_id', privateEvent.event_id);
|
|
76
|
+
|
|
77
|
+
// Should return empty (view filters by public_readable = true)
|
|
78
|
+
expect(data).toEqual([]);
|
|
79
|
+
}, TEST_TIMEOUT);
|
|
80
|
+
|
|
81
|
+
it('should block anonymous access when event.is_visible = false', async () => {
|
|
82
|
+
const { data, error } = await anonClient
|
|
83
|
+
.from('public_recipe_details')
|
|
84
|
+
.select('*')
|
|
85
|
+
.eq('event_id', 'hidden-event-1');
|
|
86
|
+
|
|
87
|
+
// Should return empty (view filters by is_visible = true)
|
|
88
|
+
expect(data).toEqual([]);
|
|
89
|
+
}, TEST_TIMEOUT);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('View Column Exposure', () => {
|
|
93
|
+
it('should expose only expected columns', async () => {
|
|
94
|
+
const { data, error } = await anonClient
|
|
95
|
+
.from('public_recipe_details')
|
|
96
|
+
.select('*')
|
|
97
|
+
.eq('event_id', publicEvent.event_id)
|
|
98
|
+
.limit(1)
|
|
99
|
+
.single();
|
|
100
|
+
|
|
101
|
+
expect(error).toBeNull();
|
|
102
|
+
if (data) {
|
|
103
|
+
// Verify expected columns exist
|
|
104
|
+
expect(data).toHaveProperty('dish_id');
|
|
105
|
+
expect(data).toHaveProperty('dish_name');
|
|
106
|
+
expect(data).toHaveProperty('dish_code');
|
|
107
|
+
expect(data).toHaveProperty('event_id');
|
|
108
|
+
expect(data).toHaveProperty('organisation_id');
|
|
109
|
+
|
|
110
|
+
// Verify sensitive columns are NOT exposed
|
|
111
|
+
// (adjust based on actual view definition)
|
|
112
|
+
// expect(data).not.toHaveProperty('created_by');
|
|
113
|
+
// expect(data).not.toHaveProperty('updated_by');
|
|
114
|
+
}
|
|
115
|
+
}, TEST_TIMEOUT);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Diet/Meal Filters', () => {
|
|
119
|
+
it('should filter by meal type', async () => {
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
const { data, error } = await anonClient
|
|
122
|
+
.from('public_recipe_details')
|
|
123
|
+
.select('*')
|
|
124
|
+
.eq('event_id', publicEvent.event_id)
|
|
125
|
+
.eq('mealtype_name', 'Breakfast');
|
|
126
|
+
const duration = Date.now() - start;
|
|
127
|
+
|
|
128
|
+
expect(error).toBeNull();
|
|
129
|
+
expect(data).toBeDefined();
|
|
130
|
+
expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
|
|
131
|
+
}, TEST_TIMEOUT);
|
|
132
|
+
|
|
133
|
+
it('should filter by diet requirements', async () => {
|
|
134
|
+
// This test would verify that diet filters work correctly
|
|
135
|
+
// Adjust based on actual view structure
|
|
136
|
+
const { data, error } = await anonClient
|
|
137
|
+
.from('public_recipe_details')
|
|
138
|
+
.select('*')
|
|
139
|
+
.eq('event_id', publicEvent.event_id)
|
|
140
|
+
.eq('dish_dietnote', 'Vegetarian');
|
|
141
|
+
|
|
142
|
+
expect(error).toBeNull();
|
|
143
|
+
expect(data).toBeDefined();
|
|
144
|
+
}, TEST_TIMEOUT);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Performance', () => {
|
|
148
|
+
it('should complete view queries in < 500ms', async () => {
|
|
149
|
+
const start = Date.now();
|
|
150
|
+
await anonClient
|
|
151
|
+
.from('public_recipe_details')
|
|
152
|
+
.select('*')
|
|
153
|
+
.eq('event_id', publicEvent.event_id)
|
|
154
|
+
.limit(100);
|
|
155
|
+
const duration = Date.now() - start;
|
|
156
|
+
|
|
157
|
+
expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
|
|
158
|
+
}, TEST_TIMEOUT);
|
|
159
|
+
|
|
160
|
+
it('should handle filtered queries efficiently', async () => {
|
|
161
|
+
const start = Date.now();
|
|
162
|
+
await anonClient
|
|
163
|
+
.from('public_recipe_details')
|
|
164
|
+
.select('*')
|
|
165
|
+
.eq('event_id', publicEvent.event_id)
|
|
166
|
+
.eq('mealtype_name', 'Breakfast')
|
|
167
|
+
.limit(50);
|
|
168
|
+
const duration = Date.now() - start;
|
|
169
|
+
|
|
170
|
+
expect(duration).toBeLessThan(PERFORMANCE_THRESHOLD);
|
|
171
|
+
}, TEST_TIMEOUT);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe.skipIf(!USE_REAL_DB || !TEST_SUPABASE_URL || !TEST_SUPABASE_ANON_KEY)('Public Recipe View - Authenticated Access', () => {
|
|
176
|
+
it('should allow authenticated users to view public recipes', async () => {
|
|
177
|
+
const { data, error } = await authenticatedClient
|
|
178
|
+
.from('public_recipe_details')
|
|
179
|
+
.select('*')
|
|
180
|
+
.eq('event_id', publicEvent.event_id);
|
|
181
|
+
|
|
182
|
+
expect(error).toBeNull();
|
|
183
|
+
expect(data).toBeDefined();
|
|
184
|
+
}, TEST_TIMEOUT);
|
|
185
|
+
|
|
186
|
+
it('should allow authenticated users to view private events they have access to', async () => {
|
|
187
|
+
// Authenticated users with organisation access should be able to view
|
|
188
|
+
// recipes from events in their organisation, even if not public_readable
|
|
189
|
+
// (This depends on the view definition and RLS policies)
|
|
190
|
+
const { data, error } = await authenticatedClient
|
|
191
|
+
.from('public_recipe_details')
|
|
192
|
+
.select('*')
|
|
193
|
+
.eq('event_id', privateEvent.event_id);
|
|
194
|
+
|
|
195
|
+
// Result depends on RLS policies and view definition
|
|
196
|
+
expect(error).toBeNull();
|
|
197
|
+
}, TEST_TIMEOUT);
|
|
198
|
+
});
|
|
199
|
+
|