@jmruthers/pace-core 0.5.38 → 0.5.41
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-SUM2QMUM.js → DataTable-2HA7VQZ4.js} +2 -2
- package/dist/{DataTable-Da0D1BHX.d.ts → DataTable-DqDDvBfI.d.ts} +1 -1
- package/dist/{chunk-2XAK7GBW.js → chunk-7SLC6HXW.js} +2 -2
- package/dist/{chunk-JHQ7DMPJ.js → chunk-MQS7SEE6.js} +57 -3
- package/dist/chunk-MQS7SEE6.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +2 -2
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/rbac/cli/policy-manager.js +278 -0
- package/dist/rbac/cli/policy-manager.js.map +1 -0
- package/dist/{types-zOqe4P1s.d.ts → types-E5WSpEtz.d.ts} +7 -0
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.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/PublicErrorBoundary.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/interfaces/AggregateConfig.md +4 -4
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +14 -14
- package/docs/api/interfaces/DataTableColumn.md +31 -5
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +7 -7
- package/docs/api/interfaces/EmptyStateConfig.md +5 -5
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +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/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/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/UsePublicRouteParamsReturn.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 +2 -2
- package/docs/implementation-guides/data-tables.md +189 -0
- package/docs/implementation-guides/datatable-filtering.md +313 -0
- package/docs/rbac/README-rbac-rls-integration.md +358 -0
- package/docs/rbac/examples/rbac-rls-integration-example.md +332 -0
- package/docs/rbac/rbac-rls-integration.md +377 -0
- package/package.json +19 -3
- package/src/components/DataTable/components/ActionButtons.tsx +22 -2
- package/src/components/DataTable/components/DataTableCore.tsx +28 -0
- package/src/components/DataTable/components/EditableRow.tsx +6 -0
- package/src/components/DataTable/components/FilterRow.tsx +14 -2
- package/src/components/DataTable/components/UnifiedTableBody.tsx +6 -0
- package/src/components/DataTable/types.ts +4 -0
- package/src/rbac/cli/policy-manager.ts +443 -0
- package/dist/chunk-JHQ7DMPJ.js.map +0 -1
- /package/dist/{DataTable-SUM2QMUM.js.map → DataTable-2HA7VQZ4.js.map} +0 -0
- /package/dist/{chunk-2XAK7GBW.js.map → chunk-7SLC6HXW.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmruthers/pace-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.41",
|
|
4
4
|
"description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -104,6 +104,10 @@
|
|
|
104
104
|
"./source/rbac": {
|
|
105
105
|
"import": "./src/rbac/index.ts",
|
|
106
106
|
"default": "./src/rbac/index.ts"
|
|
107
|
+
},
|
|
108
|
+
"./rbac/cli": {
|
|
109
|
+
"import": "./dist/rbac/cli/policy-manager.js",
|
|
110
|
+
"default": "./dist/rbac/cli/policy-manager.js"
|
|
107
111
|
}
|
|
108
112
|
},
|
|
109
113
|
"files": [
|
|
@@ -151,7 +155,15 @@
|
|
|
151
155
|
"docs:generate": "node scripts/generate-docs.js generate",
|
|
152
156
|
"docs:update-index": "node scripts/generate-docs.js update-index",
|
|
153
157
|
"docs:generate-api": "node scripts/generate-docs.js generate-api",
|
|
154
|
-
"docs:all": "node scripts/generate-docs.js all"
|
|
158
|
+
"docs:all": "node scripts/generate-docs.js all",
|
|
159
|
+
"_comment_rbac": "RBAC policy management utilities",
|
|
160
|
+
"rbac:list": "node dist/rbac/cli/policy-manager.js list",
|
|
161
|
+
"rbac:register": "node dist/rbac/cli/policy-manager.js register",
|
|
162
|
+
"rbac:update": "node dist/rbac/cli/policy-manager.js update",
|
|
163
|
+
"rbac:update-all": "node dist/rbac/cli/policy-manager.js update-all",
|
|
164
|
+
"rbac:health": "node dist/rbac/cli/policy-manager.js health",
|
|
165
|
+
"rbac:audit": "node dist/rbac/cli/policy-manager.js audit",
|
|
166
|
+
"rbac:test": "node dist/rbac/cli/policy-manager.js test"
|
|
155
167
|
},
|
|
156
168
|
"keywords": [
|
|
157
169
|
"react",
|
|
@@ -209,6 +221,10 @@
|
|
|
209
221
|
},
|
|
210
222
|
"dependencies": {
|
|
211
223
|
"@supabase/supabase-js": "^2.49.10",
|
|
212
|
-
"@tanstack/react-query": "^5.56.2"
|
|
224
|
+
"@tanstack/react-query": "^5.56.2",
|
|
225
|
+
"commander": "^12.0.0",
|
|
226
|
+
"chalk": "^5.3.0",
|
|
227
|
+
"ora": "^8.0.1",
|
|
228
|
+
"cli-table3": "^0.6.3"
|
|
213
229
|
}
|
|
214
230
|
}
|
|
@@ -135,7 +135,17 @@ export function ActionButtons({
|
|
|
135
135
|
key={actionIndex}
|
|
136
136
|
variant={action.variant === 'destructive' ? 'destructive' : 'ghost'}
|
|
137
137
|
size="sm"
|
|
138
|
-
onClick={() =>
|
|
138
|
+
onClick={() => {
|
|
139
|
+
// DEBUG: Log action click
|
|
140
|
+
if (process.env.NODE_ENV === 'development') {
|
|
141
|
+
console.log('[ActionButtons] Action clicked:', {
|
|
142
|
+
actionLabel: action.label,
|
|
143
|
+
rowOriginal: row.original,
|
|
144
|
+
actionOnClick: typeof action.onClick
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
action.onClick(row.original);
|
|
148
|
+
}}
|
|
139
149
|
disabled={isDisabled}
|
|
140
150
|
aria-disabled={isDisabled}
|
|
141
151
|
data-testid={action.testId}
|
|
@@ -183,7 +193,17 @@ export function ActionButtons({
|
|
|
183
193
|
<SelectItem
|
|
184
194
|
key={actionIndex}
|
|
185
195
|
value={`action-${actionIndex}`}
|
|
186
|
-
onClick={() =>
|
|
196
|
+
onClick={() => {
|
|
197
|
+
// DEBUG: Log action click
|
|
198
|
+
if (process.env.NODE_ENV === 'development') {
|
|
199
|
+
console.log('[ActionButtons] Dropdown action clicked:', {
|
|
200
|
+
actionLabel: action.label,
|
|
201
|
+
rowOriginal: row.original,
|
|
202
|
+
actionOnClick: typeof action.onClick
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
action.onClick(row.original);
|
|
206
|
+
}}
|
|
187
207
|
className={action.variant === 'destructive' ? 'text-acc-600' : ''}
|
|
188
208
|
>
|
|
189
209
|
{Icon && <Icon className="mr-2 h-4 w-4" />}
|
|
@@ -687,6 +687,25 @@ function DataTableInternal<TData extends DataRecord>({
|
|
|
687
687
|
// Create a new array to avoid mutating the original
|
|
688
688
|
const result = [...actions];
|
|
689
689
|
|
|
690
|
+
// DEBUG: Log action configuration
|
|
691
|
+
if (process.env.NODE_ENV === 'development') {
|
|
692
|
+
console.log('[DataTable] Action Configuration Debug:', {
|
|
693
|
+
originalActions: actions.length,
|
|
694
|
+
secureFeatures: {
|
|
695
|
+
editing: secureFeatures.editing,
|
|
696
|
+
deletion: secureFeatures.deletion
|
|
697
|
+
},
|
|
698
|
+
secureHandlers: {
|
|
699
|
+
onEditRow: !!secureHandlers.onEditRow,
|
|
700
|
+
onDeleteRow: !!secureHandlers.onDeleteRow
|
|
701
|
+
},
|
|
702
|
+
permissions: {
|
|
703
|
+
canUpdate: permissions.canUpdate.can,
|
|
704
|
+
canDelete: permissions.canDelete.can
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
690
709
|
// Add Edit action with RBAC check
|
|
691
710
|
if (secureFeatures.editing && secureHandlers.onEditRow && !result.some(a => a.label === 'Edit')) {
|
|
692
711
|
result.push({
|
|
@@ -723,6 +742,15 @@ function DataTableInternal<TData extends DataRecord>({
|
|
|
723
742
|
});
|
|
724
743
|
}
|
|
725
744
|
|
|
745
|
+
// DEBUG: Log final actions
|
|
746
|
+
if (process.env.NODE_ENV === 'development') {
|
|
747
|
+
console.log('[DataTable] Final Actions:', {
|
|
748
|
+
totalActions: result.length,
|
|
749
|
+
actionLabels: result.map(a => a.label),
|
|
750
|
+
hiddenActions: result.filter(a => a.hidden).map(a => a.label)
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
726
754
|
return result;
|
|
727
755
|
}, [actions, secureFeatures, permissions, secureHandlers, getRowId, tableActions]);
|
|
728
756
|
|
|
@@ -28,6 +28,12 @@ const renderEditField = (
|
|
|
28
28
|
) => {
|
|
29
29
|
const columnDef = column.columnDef;
|
|
30
30
|
|
|
31
|
+
// Check if column is editable (default: true)
|
|
32
|
+
if (columnDef.editable === false) {
|
|
33
|
+
// Return the original value as text if column is not editable
|
|
34
|
+
return <span className="text-sm text-gray-600">{value || ''}</span>;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
// Check for custom field type
|
|
32
38
|
if (columnDef.fieldType === 'select' && columnDef.fieldOptions) {
|
|
33
39
|
// Use editAccessorKey if specified, otherwise use the column id
|
|
@@ -16,8 +16,14 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
16
16
|
const column = table.getColumn(columnId);
|
|
17
17
|
if (!column) return [];
|
|
18
18
|
|
|
19
|
-
// Check if column has explicit field options
|
|
20
19
|
const columnDef = column.columnDef as any;
|
|
20
|
+
|
|
21
|
+
// Check for filterSelectOptions first (preferred for filters)
|
|
22
|
+
if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
|
|
23
|
+
return columnDef.filterSelectOptions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if column has explicit field options
|
|
21
27
|
if (columnDef.fieldOptions && Array.isArray(columnDef.fieldOptions)) {
|
|
22
28
|
return columnDef.fieldOptions;
|
|
23
29
|
}
|
|
@@ -41,11 +47,17 @@ export function FilterRow<TData>({ table, visibleColumns }: FilterRowProps<TData
|
|
|
41
47
|
const column = table.getColumn(columnId);
|
|
42
48
|
if (!column) return 'text';
|
|
43
49
|
|
|
44
|
-
// Check if column has explicit filter type configuration
|
|
45
50
|
const columnDef = column.columnDef as any;
|
|
51
|
+
|
|
52
|
+
// Check if column has explicit filter type configuration
|
|
46
53
|
if (columnDef.filterType) {
|
|
47
54
|
return columnDef.filterType;
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
// Auto-detect select filter if filterSelectOptions is provided
|
|
58
|
+
if (columnDef.filterSelectOptions && Array.isArray(columnDef.filterSelectOptions)) {
|
|
59
|
+
return 'select';
|
|
60
|
+
}
|
|
49
61
|
|
|
50
62
|
// Check if it's a date column
|
|
51
63
|
if (columnId.toLowerCase().includes('date') || columnId.toLowerCase().includes('time')) {
|
|
@@ -156,6 +156,12 @@ const renderEditField = (
|
|
|
156
156
|
) => {
|
|
157
157
|
const columnDef = column.columnDef;
|
|
158
158
|
|
|
159
|
+
// Check if column is editable (default: true)
|
|
160
|
+
if (columnDef.editable === false) {
|
|
161
|
+
// Return the original value as text if column is not editable
|
|
162
|
+
return <span className="text-sm text-gray-600">{value || ''}</span>;
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
// Check for custom field type
|
|
160
166
|
if (columnDef.fieldType === 'select' && columnDef.fieldOptions) {
|
|
161
167
|
// Use editAccessorKey if specified, otherwise use the column id
|
|
@@ -234,6 +234,10 @@ export interface DataTableColumn<TData extends DataRecord = DataRecord> extends
|
|
|
234
234
|
fieldOptions?: Array<{ value: string | number; label: string }>;
|
|
235
235
|
/** Filter type for column filtering (text, select, number, date) */
|
|
236
236
|
filterType?: 'text' | 'select' | 'number' | 'date';
|
|
237
|
+
/** Options for select filters (alternative to fieldOptions) */
|
|
238
|
+
filterSelectOptions?: Array<{ value: string | number; label: string }>;
|
|
239
|
+
/** Whether this column is editable in edit mode (default: true) */
|
|
240
|
+
editable?: boolean;
|
|
237
241
|
|
|
238
242
|
// Hierarchical rendering
|
|
239
243
|
/** Custom renderer for parent rows */
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RBAC Policy Manager CLI Tool
|
|
5
|
+
*
|
|
6
|
+
* This tool provides command-line interface for managing RBAC-RLS integration policies.
|
|
7
|
+
* It allows developers and administrators to easily manage dynamic permission policies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createClient } from '@supabase/supabase-js';
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import Table from 'cli-table3';
|
|
15
|
+
|
|
16
|
+
interface PolicyConfig {
|
|
17
|
+
table_name: string;
|
|
18
|
+
page_name: string;
|
|
19
|
+
app_name: string;
|
|
20
|
+
organisation_column: string;
|
|
21
|
+
event_column?: string;
|
|
22
|
+
operations: string[];
|
|
23
|
+
is_active: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PolicyAudit {
|
|
27
|
+
table_name: string;
|
|
28
|
+
policy_name: string;
|
|
29
|
+
operation: string;
|
|
30
|
+
action: string;
|
|
31
|
+
changed_at: string;
|
|
32
|
+
success: boolean;
|
|
33
|
+
error_message?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface HealthCheck {
|
|
37
|
+
table_name: string;
|
|
38
|
+
policy_name: string;
|
|
39
|
+
operation: string;
|
|
40
|
+
is_healthy: boolean;
|
|
41
|
+
issues: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class PolicyManager {
|
|
45
|
+
private supabase: any;
|
|
46
|
+
|
|
47
|
+
constructor(url: string, key: string) {
|
|
48
|
+
this.supabase = createClient(url, key);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* List all registered tables
|
|
53
|
+
*/
|
|
54
|
+
async listTables(): Promise<void> {
|
|
55
|
+
const spinner = ora('Fetching registered tables...').start();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { data, error } = await this.supabase
|
|
59
|
+
.from('rbac_policy_configs')
|
|
60
|
+
.select('*')
|
|
61
|
+
.eq('is_active', true)
|
|
62
|
+
.order('table_name');
|
|
63
|
+
|
|
64
|
+
if (error) throw error;
|
|
65
|
+
|
|
66
|
+
spinner.succeed('Fetched registered tables');
|
|
67
|
+
|
|
68
|
+
if (data.length === 0) {
|
|
69
|
+
console.log(chalk.yellow('No tables registered for RBAC policy management.'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const table = new Table({
|
|
74
|
+
head: ['Table', 'Page', 'App', 'Org Column', 'Event Column', 'Operations'],
|
|
75
|
+
colWidths: [20, 15, 10, 15, 15, 30]
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
data.forEach((config: PolicyConfig) => {
|
|
79
|
+
table.push([
|
|
80
|
+
config.table_name,
|
|
81
|
+
config.page_name,
|
|
82
|
+
config.app_name,
|
|
83
|
+
config.organisation_column,
|
|
84
|
+
config.event_column || 'N/A',
|
|
85
|
+
config.operations.join(', ')
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log(table.toString());
|
|
90
|
+
} catch (error) {
|
|
91
|
+
spinner.fail('Failed to fetch tables');
|
|
92
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a new table for RBAC policy management
|
|
98
|
+
*/
|
|
99
|
+
async registerTable(
|
|
100
|
+
tableName: string,
|
|
101
|
+
pageName: string,
|
|
102
|
+
appName: string = 'CAKE',
|
|
103
|
+
orgColumn: string = 'organisation_id',
|
|
104
|
+
eventColumn?: string,
|
|
105
|
+
operations: string[] = ['read', 'create', 'update', 'delete']
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const spinner = ora(`Registering table ${tableName}...`).start();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const { data, error } = await this.supabase
|
|
111
|
+
.rpc('register_rbac_table', {
|
|
112
|
+
p_table_name: tableName,
|
|
113
|
+
p_page_name: pageName,
|
|
114
|
+
p_app_name: appName,
|
|
115
|
+
p_organisation_column: orgColumn,
|
|
116
|
+
p_event_column: eventColumn,
|
|
117
|
+
p_operations: operations
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (error) throw error;
|
|
121
|
+
|
|
122
|
+
if (data) {
|
|
123
|
+
spinner.succeed(`Successfully registered table ${tableName}`);
|
|
124
|
+
} else {
|
|
125
|
+
spinner.fail(`Failed to register table ${tableName}`);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
spinner.fail(`Failed to register table ${tableName}`);
|
|
129
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Update policies for a specific table
|
|
135
|
+
*/
|
|
136
|
+
async updateTable(tableName: string, pageName: string, appName: string = 'CAKE'): Promise<void> {
|
|
137
|
+
const spinner = ora(`Updating policies for ${tableName}...`).start();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const { data, error } = await this.supabase
|
|
141
|
+
.rpc('update_rbac_policies_for_table', {
|
|
142
|
+
p_table_name: tableName,
|
|
143
|
+
p_page_name: pageName,
|
|
144
|
+
p_app_name: appName
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (error) throw error;
|
|
148
|
+
|
|
149
|
+
if (data) {
|
|
150
|
+
spinner.succeed(`Successfully updated policies for ${tableName}`);
|
|
151
|
+
} else {
|
|
152
|
+
spinner.fail(`Failed to update policies for ${tableName}`);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
spinner.fail(`Failed to update policies for ${tableName}`);
|
|
156
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Update all RBAC policies
|
|
162
|
+
*/
|
|
163
|
+
async updateAll(): Promise<void> {
|
|
164
|
+
const spinner = ora('Updating all RBAC policies...').start();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const { data, error } = await this.supabase
|
|
168
|
+
.rpc('update_all_rbac_policies');
|
|
169
|
+
|
|
170
|
+
if (error) throw error;
|
|
171
|
+
|
|
172
|
+
spinner.succeed('Updated all RBAC policies');
|
|
173
|
+
|
|
174
|
+
// Display results
|
|
175
|
+
const table = new Table({
|
|
176
|
+
head: ['Table', 'Page', 'App', 'Success', 'Error'],
|
|
177
|
+
colWidths: [20, 15, 10, 10, 30]
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
data.forEach((result: any) => {
|
|
181
|
+
table.push([
|
|
182
|
+
result.table_name,
|
|
183
|
+
result.page_name,
|
|
184
|
+
result.app_name,
|
|
185
|
+
result.success ? chalk.green('✓') : chalk.red('✗'),
|
|
186
|
+
result.error_message || ''
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log(table.toString());
|
|
191
|
+
} catch (error) {
|
|
192
|
+
spinner.fail('Failed to update all policies');
|
|
193
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check policy health
|
|
199
|
+
*/
|
|
200
|
+
async checkHealth(): Promise<void> {
|
|
201
|
+
const spinner = ora('Checking policy health...').start();
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const { data, error } = await this.supabase
|
|
205
|
+
.rpc('check_rbac_policy_health');
|
|
206
|
+
|
|
207
|
+
if (error) throw error;
|
|
208
|
+
|
|
209
|
+
spinner.succeed('Policy health check complete');
|
|
210
|
+
|
|
211
|
+
if (data.length === 0) {
|
|
212
|
+
console.log(chalk.yellow('No policies found.'));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const healthyPolicies = data.filter((h: HealthCheck) => h.is_healthy);
|
|
217
|
+
const unhealthyPolicies = data.filter((h: HealthCheck) => !h.is_healthy);
|
|
218
|
+
|
|
219
|
+
console.log(chalk.green(`✓ ${healthyPolicies.length} healthy policies`));
|
|
220
|
+
console.log(chalk.red(`✗ ${unhealthyPolicies.length} unhealthy policies`));
|
|
221
|
+
|
|
222
|
+
if (unhealthyPolicies.length > 0) {
|
|
223
|
+
console.log('\n' + chalk.red('Unhealthy Policies:'));
|
|
224
|
+
const table = new Table({
|
|
225
|
+
head: ['Table', 'Policy', 'Operation', 'Issues'],
|
|
226
|
+
colWidths: [20, 25, 10, 40]
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
unhealthyPolicies.forEach((policy: HealthCheck) => {
|
|
230
|
+
table.push([
|
|
231
|
+
policy.table_name,
|
|
232
|
+
policy.policy_name,
|
|
233
|
+
policy.operation,
|
|
234
|
+
policy.issues.join(', ')
|
|
235
|
+
]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
console.log(table.toString());
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
spinner.fail('Failed to check policy health');
|
|
242
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Show policy audit log
|
|
248
|
+
*/
|
|
249
|
+
async showAudit(limit: number = 20): Promise<void> {
|
|
250
|
+
const spinner = ora('Fetching audit log...').start();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const { data, error } = await this.supabase
|
|
254
|
+
.from('rbac_policy_audit')
|
|
255
|
+
.select('*')
|
|
256
|
+
.order('changed_at', { ascending: false })
|
|
257
|
+
.limit(limit);
|
|
258
|
+
|
|
259
|
+
if (error) throw error;
|
|
260
|
+
|
|
261
|
+
spinner.succeed('Fetched audit log');
|
|
262
|
+
|
|
263
|
+
if (data.length === 0) {
|
|
264
|
+
console.log(chalk.yellow('No audit entries found.'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const table = new Table({
|
|
269
|
+
head: ['Table', 'Policy', 'Operation', 'Action', 'Success', 'Changed At'],
|
|
270
|
+
colWidths: [15, 20, 10, 10, 8, 20]
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
data.forEach((audit: PolicyAudit) => {
|
|
274
|
+
table.push([
|
|
275
|
+
audit.table_name,
|
|
276
|
+
audit.policy_name,
|
|
277
|
+
audit.operation,
|
|
278
|
+
audit.action,
|
|
279
|
+
audit.success ? chalk.green('✓') : chalk.red('✗'),
|
|
280
|
+
new Date(audit.changed_at).toLocaleString()
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
console.log(table.toString());
|
|
285
|
+
} catch (error) {
|
|
286
|
+
spinner.fail('Failed to fetch audit log');
|
|
287
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Test permission check
|
|
293
|
+
*/
|
|
294
|
+
async testPermission(
|
|
295
|
+
operation: string,
|
|
296
|
+
pageName: string,
|
|
297
|
+
orgId: string,
|
|
298
|
+
eventId?: string,
|
|
299
|
+
appName: string = 'CAKE'
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
const spinner = ora('Testing permission check...').start();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const { data, error } = await this.supabase
|
|
305
|
+
.rpc('check_rbac_permission_with_context', {
|
|
306
|
+
p_operation: operation,
|
|
307
|
+
p_page_name: pageName,
|
|
308
|
+
p_resource_organisation_id: orgId,
|
|
309
|
+
p_resource_event_id: eventId,
|
|
310
|
+
p_app_name: appName
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (error) throw error;
|
|
314
|
+
|
|
315
|
+
spinner.succeed('Permission check complete');
|
|
316
|
+
|
|
317
|
+
const result = data ? chalk.green('ALLOWED') : chalk.red('DENIED');
|
|
318
|
+
console.log(`Permission: ${operation} on ${pageName} = ${result}`);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
spinner.fail('Failed to test permission');
|
|
321
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// CLI Setup
|
|
327
|
+
const program = new Command();
|
|
328
|
+
|
|
329
|
+
program
|
|
330
|
+
.name('rbac-policy-manager')
|
|
331
|
+
.description('CLI tool for managing RBAC-RLS integration policies')
|
|
332
|
+
.version('1.0.0');
|
|
333
|
+
|
|
334
|
+
// Global options
|
|
335
|
+
program
|
|
336
|
+
.option('-u, --url <url>', 'Supabase URL', process.env.SUPABASE_URL)
|
|
337
|
+
.option('-k, --key <key>', 'Supabase service key', process.env.SUPABASE_SERVICE_ROLE_KEY)
|
|
338
|
+
.option('--verbose', 'Enable verbose output');
|
|
339
|
+
|
|
340
|
+
// List tables command
|
|
341
|
+
program
|
|
342
|
+
.command('list')
|
|
343
|
+
.description('List all registered tables')
|
|
344
|
+
.action(async (options) => {
|
|
345
|
+
const url = options.parent?.url || process.env.SUPABASE_URL;
|
|
346
|
+
const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
347
|
+
const manager = new PolicyManager(url, key);
|
|
348
|
+
await manager.listTables();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Register table command
|
|
352
|
+
program
|
|
353
|
+
.command('register <tableName> <pageName>')
|
|
354
|
+
.description('Register a table for RBAC policy management')
|
|
355
|
+
.option('-a, --app <appName>', 'App name', 'CAKE')
|
|
356
|
+
.option('-o, --org-column <column>', 'Organisation column name', 'organisation_id')
|
|
357
|
+
.option('-e, --event-column <column>', 'Event column name')
|
|
358
|
+
.option('--operations <operations>', 'Comma-separated operations', 'read,create,update,delete')
|
|
359
|
+
.action(async (tableName, pageName, options) => {
|
|
360
|
+
const operations = options.operations.split(',').map((op: string) => op.trim());
|
|
361
|
+
const url = options.parent?.url || process.env.SUPABASE_URL;
|
|
362
|
+
const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
363
|
+
const manager = new PolicyManager(url, key);
|
|
364
|
+
await manager.registerTable(
|
|
365
|
+
tableName,
|
|
366
|
+
pageName,
|
|
367
|
+
options.app,
|
|
368
|
+
options.orgColumn,
|
|
369
|
+
options.eventColumn,
|
|
370
|
+
operations
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Update table command
|
|
375
|
+
program
|
|
376
|
+
.command('update <tableName> <pageName>')
|
|
377
|
+
.description('Update policies for a specific table')
|
|
378
|
+
.option('-a, --app <appName>', 'App name', 'CAKE')
|
|
379
|
+
.action(async (tableName, pageName, options) => {
|
|
380
|
+
const url = options.parent?.url || process.env.SUPABASE_URL;
|
|
381
|
+
const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
382
|
+
const manager = new PolicyManager(url, key);
|
|
383
|
+
await manager.updateTable(tableName, pageName, options.app);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Update all command
|
|
387
|
+
program
|
|
388
|
+
.command('update-all')
|
|
389
|
+
.description('Update all RBAC policies')
|
|
390
|
+
.action(async (options) => {
|
|
391
|
+
const url = options.url || process.env.SUPABASE_URL;
|
|
392
|
+
const key = options.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
393
|
+
const manager = new PolicyManager(url, key);
|
|
394
|
+
await manager.updateAll();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Health check command
|
|
398
|
+
program
|
|
399
|
+
.command('health')
|
|
400
|
+
.description('Check policy health')
|
|
401
|
+
.action(async (options) => {
|
|
402
|
+
const url = options.url || process.env.SUPABASE_URL;
|
|
403
|
+
const key = options.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
404
|
+
const manager = new PolicyManager(url, key);
|
|
405
|
+
await manager.checkHealth();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Audit command
|
|
409
|
+
program
|
|
410
|
+
.command('audit')
|
|
411
|
+
.description('Show policy audit log')
|
|
412
|
+
.option('-l, --limit <number>', 'Number of entries to show', '20')
|
|
413
|
+
.action(async (options) => {
|
|
414
|
+
const url = options.parent?.url || process.env.SUPABASE_URL;
|
|
415
|
+
const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
416
|
+
const manager = new PolicyManager(url, key);
|
|
417
|
+
await manager.showAudit(parseInt(options.limit));
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Test permission command
|
|
421
|
+
program
|
|
422
|
+
.command('test <operation> <pageName> <orgId>')
|
|
423
|
+
.description('Test a permission check')
|
|
424
|
+
.option('-e, --event-id <eventId>', 'Event ID')
|
|
425
|
+
.option('-a, --app <appName>', 'App name', 'CAKE')
|
|
426
|
+
.action(async (operation, pageName, orgId, options) => {
|
|
427
|
+
const url = options.parent?.url || process.env.SUPABASE_URL;
|
|
428
|
+
const key = options.parent?.key || process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
429
|
+
const manager = new PolicyManager(url, key);
|
|
430
|
+
await manager.testPermission(operation, pageName, orgId, options.eventId, options.app);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Parse command line arguments
|
|
434
|
+
program.parse(process.argv);
|
|
435
|
+
|
|
436
|
+
// Handle missing required options
|
|
437
|
+
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
438
|
+
console.error(chalk.red('Error: Missing required environment variables'));
|
|
439
|
+
console.error('Please set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY');
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export { PolicyManager };
|