hazo_auth 3.0.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +146 -0
  2. package/SETUP_CHECKLIST.md +369 -0
  3. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
  4. package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
  5. package/dist/components/layouts/rbac_test/index.d.ts +15 -0
  6. package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
  7. package/dist/components/layouts/rbac_test/index.js +378 -0
  8. package/dist/components/layouts/shared/components/password_field.js +1 -1
  9. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  10. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
  11. package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
  12. package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
  13. package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
  14. package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
  15. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
  16. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
  17. package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
  18. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
  19. package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
  20. package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
  21. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
  22. package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
  23. package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
  24. package/dist/components/layouts/user_management/index.d.ts +9 -2
  25. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  26. package/dist/components/layouts/user_management/index.js +22 -6
  27. package/dist/components/ui/select.d.ts +14 -0
  28. package/dist/components/ui/select.d.ts.map +1 -0
  29. package/dist/components/ui/select.js +59 -0
  30. package/dist/components/ui/tree-view.d.ts +108 -0
  31. package/dist/components/ui/tree-view.d.ts.map +1 -0
  32. package/dist/components/ui/tree-view.js +194 -0
  33. package/dist/lib/auth/auth_types.d.ts +45 -0
  34. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  35. package/dist/lib/auth/auth_types.js +13 -0
  36. package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
  37. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  38. package/dist/lib/auth/hazo_get_auth.server.js +107 -3
  39. package/dist/lib/auth/scope_cache.d.ts +92 -0
  40. package/dist/lib/auth/scope_cache.d.ts.map +1 -0
  41. package/dist/lib/auth/scope_cache.js +171 -0
  42. package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
  43. package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
  44. package/dist/lib/scope_hierarchy_config.server.js +96 -0
  45. package/dist/lib/services/email_service.d.ts.map +1 -1
  46. package/dist/lib/services/email_service.js +7 -2
  47. package/dist/lib/services/profile_picture_service.d.ts +1 -7
  48. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  49. package/dist/lib/services/profile_picture_service.js +77 -32
  50. package/dist/lib/services/registration_service.js +1 -1
  51. package/dist/lib/services/scope_labels_service.d.ts +48 -0
  52. package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
  53. package/dist/lib/services/scope_labels_service.js +277 -0
  54. package/dist/lib/services/scope_service.d.ts +114 -0
  55. package/dist/lib/services/scope_service.d.ts.map +1 -0
  56. package/dist/lib/services/scope_service.js +582 -0
  57. package/dist/lib/services/user_scope_service.d.ts +74 -0
  58. package/dist/lib/services/user_scope_service.d.ts.map +1 -0
  59. package/dist/lib/services/user_scope_service.js +415 -0
  60. package/hazo_auth_config.example.ini +1 -1
  61. package/package.json +3 -1
package/README.md CHANGED
@@ -706,6 +706,7 @@ import { ResetPasswordLayout } from "hazo_auth/components/layouts/reset_password
706
706
  import { EmailVerificationLayout } from "hazo_auth/components/layouts/email_verification";
707
707
  import { MySettingsLayout } from "hazo_auth/components/layouts/my_settings";
708
708
  import { UserManagementLayout } from "hazo_auth/components/layouts/user_management";
709
+ import { RbacTestLayout } from "hazo_auth/components/layouts/rbac_test";
709
710
 
710
711
  // Shared layout components and hooks (barrel import - recommended)
711
712
  import {
@@ -762,6 +763,7 @@ export default async function LoginPage() {
762
763
  - `EmailVerificationLayout` - Verify email address
763
764
  - `MySettingsLayout` - User profile and settings
764
765
  - `UserManagementLayout` - Admin user/role management (requires user_management API routes)
766
+ - `RbacTestLayout` - RBAC/HRBAC permission and scope testing tool (requires admin_test_access permission)
765
767
 
766
768
  ### User Management Component
767
769
 
@@ -793,6 +795,7 @@ export { GET, POST, PUT } from "hazo_auth/server/routes";
793
795
  - **Users:** List users, deactivate users, send password reset emails
794
796
  - **Permissions:** List permissions (from DB and config), migrate config permissions to DB, create/update/delete permissions
795
797
  - **Roles:** List roles with permissions, create roles, update role-permission assignments
798
+ - **UI Enhancement**: The Roles tab uses a tag-based UI for better readability. Each role displays permissions as inline tags/chips (showing up to 4, with "+N more" to expand). Edit permissions via an interactive dialog with Select All/Unselect All buttons.
796
799
  - **User Roles:** Get user roles, assign roles to users, bulk update user role assignments
797
800
 
798
801
  **Example Usage:**
@@ -1229,6 +1232,149 @@ enable_friendly_error_messages = true
1229
1232
 
1230
1233
  ---
1231
1234
 
1235
+ ## Hierarchical Role-Based Access Control (HRBAC)
1236
+
1237
+ hazo_auth supports optional Hierarchical Role-Based Access Control (HRBAC) with 7 scope levels (L1-L7). HRBAC extends standard RBAC by allowing users to be assigned to scopes in a hierarchy, with automatic inheritance of access to child scopes.
1238
+
1239
+ ### Enabling HRBAC
1240
+
1241
+ Add the following to your `hazo_auth_config.ini`:
1242
+
1243
+ ```ini
1244
+ [hazo_auth__scope_hierarchy]
1245
+ enable_hrbac = true
1246
+ default_org = my_organization # Optional: default organization for single-tenant apps
1247
+ scope_cache_ttl_minutes = 15
1248
+ scope_cache_max_entries = 5000
1249
+
1250
+ # Optional: customize default labels for each scope level
1251
+ default_label_l1 = Company
1252
+ default_label_l2 = Division
1253
+ default_label_l3 = Department
1254
+ default_label_l4 = Team
1255
+ default_label_l5 = Project
1256
+ default_label_l6 = Sub-project
1257
+ default_label_l7 = Task
1258
+ ```
1259
+
1260
+ ### Database Setup
1261
+
1262
+ HRBAC requires additional database tables. See `SETUP_CHECKLIST.md` for full PostgreSQL and SQLite scripts, including:
1263
+ - `hazo_scopes_l1` through `hazo_scopes_l7` - Scope tables with parent_scope_id references
1264
+ - `hazo_user_scopes` - User-scope assignments
1265
+ - `hazo_scope_labels` - Custom labels per organization
1266
+ - `hazo_enum_scope_types` - Enum type for scope validation
1267
+
1268
+ ### Using hazo_get_auth with Scope Options
1269
+
1270
+ When HRBAC is enabled, you can check scope access alongside permissions:
1271
+
1272
+ ```typescript
1273
+ import { hazo_get_auth } from "hazo_auth/lib/auth/hazo_get_auth.server";
1274
+ import { ScopeAccessError } from "hazo_auth/lib/auth/auth_types";
1275
+
1276
+ export async function GET(request: NextRequest) {
1277
+ try {
1278
+ const authResult = await hazo_get_auth(request, {
1279
+ required_permissions: ["view_reports"],
1280
+ scope_type: "hazo_scopes_l3", // Check access to Level 3 scope
1281
+ scope_seq: "L3_001", // Scope identifier (or use scope_id for UUID)
1282
+ strict: true, // Throws ScopeAccessError if denied
1283
+ });
1284
+
1285
+ if (!authResult.authenticated) {
1286
+ return NextResponse.json({ error: "Authentication required" }, { status: 401 });
1287
+ }
1288
+
1289
+ // Both permission_ok and scope_ok must be true for full access
1290
+ if (authResult.scope_ok) {
1291
+ // Access granted - scope_access_via shows how access was granted
1292
+ console.log("Access via:", authResult.scope_access_via);
1293
+ }
1294
+
1295
+ return NextResponse.json({ message: "Access granted" });
1296
+ } catch (error) {
1297
+ if (error instanceof ScopeAccessError) {
1298
+ return NextResponse.json(
1299
+ { error: "Scope access denied", scope: error.scope_identifier },
1300
+ { status: 403 }
1301
+ );
1302
+ }
1303
+ throw error;
1304
+ }
1305
+ }
1306
+ ```
1307
+
1308
+ ### Scope Access Inheritance
1309
+
1310
+ Users assigned to a higher-level scope automatically have access to all descendant scopes:
1311
+ - User with L2 scope access can access all L3, L4, L5, L6, L7 scopes under that L2 scope
1312
+ - Direct assignments take precedence over inherited access
1313
+ - The `scope_access_via` field in the result shows which scope granted access
1314
+
1315
+ ### Required Permissions for Management
1316
+
1317
+ - `admin_scope_hierarchy_management` - Manage scopes and scope labels
1318
+ - `admin_user_scope_assignment` - Assign scopes to users
1319
+ - `admin_test_access` - Access the RBAC/HRBAC test tool
1320
+
1321
+ Add these to your `application_permission_list_defaults` in `hazo_auth_config.ini`:
1322
+
1323
+ ```ini
1324
+ [hazo_auth__user_management]
1325
+ application_permission_list_defaults = admin_user_management,admin_role_management,admin_permission_management,admin_scope_hierarchy_management,admin_user_scope_assignment,admin_test_access
1326
+ ```
1327
+
1328
+ ### User Management UI
1329
+
1330
+ When HRBAC is enabled and the user has appropriate permissions, three new tabs appear in the User Management layout:
1331
+ - **Scope Hierarchy** - Create, edit, and delete scopes at each level
1332
+ - **Scope Labels** - Customize labels for scope levels per organization
1333
+ - **User Scopes** - Assign and remove scope assignments for users
1334
+
1335
+ ### RBAC/HRBAC Test Tool
1336
+
1337
+ The `RbacTestLayout` component provides a comprehensive testing interface for administrators to test RBAC permissions and HRBAC scope access for any user in the system.
1338
+
1339
+ **Features:**
1340
+ - **User Selection**: Dropdown to select any user in the system
1341
+ - **User Info Display**: Shows selected user's current permissions and assigned scopes
1342
+ - **RBAC Test Tab**: Select permissions to test if the user has them
1343
+ - **HRBAC Test Tab**: Select a scope from a tree view and test if the user has access
1344
+ - **Results Display**: Clear pass/fail indicators with missing permissions and scope access details
1345
+
1346
+ **Required Permission:** `admin_test_access`
1347
+
1348
+ **Usage in Your App:**
1349
+
1350
+ ```typescript
1351
+ // app/admin/rbac-test/page.tsx
1352
+ import { RbacTestLayout } from "hazo_auth/components/layouts/rbac_test";
1353
+ import { is_hrbac_enabled, get_default_org } from "hazo_auth/lib/scope_hierarchy_config.server";
1354
+
1355
+ export default function RbacTestPage() {
1356
+ const hrbacEnabled = is_hrbac_enabled();
1357
+ const defaultOrg = get_default_org();
1358
+
1359
+ return (
1360
+ <RbacTestLayout
1361
+ hrbacEnabled={hrbacEnabled}
1362
+ defaultOrg={defaultOrg}
1363
+ />
1364
+ );
1365
+ }
1366
+ ```
1367
+
1368
+ **API Route Required:**
1369
+ The test tool uses the `/api/hazo_auth/rbac_test` endpoint which is included in the package. This route:
1370
+ - Accepts `test_user_id` parameter to test any user
1371
+ - Checks permissions and scope access for the specified user
1372
+ - Requires `admin_test_access` permission to call
1373
+
1374
+ **Demo Page:** A test page is available at `/hazo_auth/rbac_test` in the demo app.
1375
+
1376
+ ---
1377
+
1232
1378
  ## Profile Picture Menu Widget
1233
1379
 
1234
1380
  The Profile Picture Menu is a versatile component for navbar or sidebar that automatically displays:
@@ -957,6 +957,375 @@ Visit each page and verify it loads:
957
957
 
958
958
  ---
959
959
 
960
+ ## Phase 7: HRBAC Setup (Optional)
961
+
962
+ Hierarchical Role-Based Access Control (HRBAC) extends the standard RBAC with 7 hierarchical scope levels. This phase is optional - only complete it if you need scope-based access control.
963
+
964
+ ### Step 7.1: Enable HRBAC in Configuration
965
+
966
+ Add to your `hazo_auth_config.ini`:
967
+
968
+ ```ini
969
+ [hazo_auth__scope_hierarchy]
970
+ enable_hrbac = true
971
+ default_org = my_organization
972
+ scope_cache_ttl_minutes = 15
973
+ scope_cache_max_entries = 5000
974
+
975
+ # Optional: customize default labels for each scope level
976
+ default_label_l1 = Company
977
+ default_label_l2 = Division
978
+ default_label_l3 = Department
979
+ default_label_l4 = Team
980
+ default_label_l5 = Project
981
+ default_label_l6 = Sub-project
982
+ default_label_l7 = Task
983
+ ```
984
+
985
+ ### Step 7.2: Add HRBAC Permissions
986
+
987
+ Add the HRBAC management permissions to your `application_permission_list_defaults`:
988
+
989
+ ```ini
990
+ [hazo_auth__user_management]
991
+ application_permission_list_defaults = admin_user_management,admin_role_management,admin_permission_management,admin_scope_hierarchy_management,admin_user_scope_assignment
992
+ ```
993
+
994
+ ### Step 7.3: Create HRBAC Database Tables
995
+
996
+ #### PostgreSQL
997
+
998
+ ```sql
999
+ -- =============================================
1000
+ -- HRBAC Database Setup Script (PostgreSQL)
1001
+ -- =============================================
1002
+
1003
+ -- 1. Create scope types enum
1004
+ CREATE TYPE hazo_enum_scope_types AS ENUM (
1005
+ 'hazo_scopes_l1',
1006
+ 'hazo_scopes_l2',
1007
+ 'hazo_scopes_l3',
1008
+ 'hazo_scopes_l4',
1009
+ 'hazo_scopes_l5',
1010
+ 'hazo_scopes_l6',
1011
+ 'hazo_scopes_l7'
1012
+ );
1013
+
1014
+ -- 2. Create sequence generator function for scope IDs
1015
+ CREATE OR REPLACE FUNCTION hazo_scope_id_generator(table_name TEXT)
1016
+ RETURNS TEXT AS $$
1017
+ DECLARE
1018
+ prefix TEXT;
1019
+ next_num INTEGER;
1020
+ result TEXT;
1021
+ BEGIN
1022
+ -- Extract level number from table name (e.g., 'hazo_scopes_l3' -> '3')
1023
+ prefix := 'L' || SUBSTRING(table_name FROM 'hazo_scopes_l([0-9]+)');
1024
+
1025
+ -- Get the next sequence number
1026
+ EXECUTE format(
1027
+ 'SELECT COALESCE(MAX(CAST(SUBSTRING(seq FROM ''L[0-9]+_([0-9]+)'') AS INTEGER)), 0) + 1 FROM %I',
1028
+ table_name
1029
+ ) INTO next_num;
1030
+
1031
+ -- Format as L{level}_{padded_number}
1032
+ result := prefix || '_' || LPAD(next_num::TEXT, 3, '0');
1033
+
1034
+ RETURN result;
1035
+ END;
1036
+ $$ LANGUAGE plpgsql;
1037
+
1038
+ -- 3. Create scope tables (L1 through L7)
1039
+
1040
+ -- Level 1 (top level - no parent)
1041
+ CREATE TABLE hazo_scopes_l1 (
1042
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1043
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l1'),
1044
+ org TEXT NOT NULL,
1045
+ name TEXT NOT NULL,
1046
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1047
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1048
+ );
1049
+ CREATE INDEX idx_hazo_scopes_l1_org ON hazo_scopes_l1(org);
1050
+ CREATE INDEX idx_hazo_scopes_l1_seq ON hazo_scopes_l1(seq);
1051
+
1052
+ -- Level 2 (parent: L1)
1053
+ CREATE TABLE hazo_scopes_l2 (
1054
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1055
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l2'),
1056
+ org TEXT NOT NULL,
1057
+ name TEXT NOT NULL,
1058
+ parent_scope_id UUID REFERENCES hazo_scopes_l1(id) ON DELETE CASCADE,
1059
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1060
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1061
+ );
1062
+ CREATE INDEX idx_hazo_scopes_l2_org ON hazo_scopes_l2(org);
1063
+ CREATE INDEX idx_hazo_scopes_l2_seq ON hazo_scopes_l2(seq);
1064
+ CREATE INDEX idx_hazo_scopes_l2_parent ON hazo_scopes_l2(parent_scope_id);
1065
+
1066
+ -- Level 3 (parent: L2)
1067
+ CREATE TABLE hazo_scopes_l3 (
1068
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1069
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l3'),
1070
+ org TEXT NOT NULL,
1071
+ name TEXT NOT NULL,
1072
+ parent_scope_id UUID REFERENCES hazo_scopes_l2(id) ON DELETE CASCADE,
1073
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1074
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1075
+ );
1076
+ CREATE INDEX idx_hazo_scopes_l3_org ON hazo_scopes_l3(org);
1077
+ CREATE INDEX idx_hazo_scopes_l3_seq ON hazo_scopes_l3(seq);
1078
+ CREATE INDEX idx_hazo_scopes_l3_parent ON hazo_scopes_l3(parent_scope_id);
1079
+
1080
+ -- Level 4 (parent: L3)
1081
+ CREATE TABLE hazo_scopes_l4 (
1082
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1083
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l4'),
1084
+ org TEXT NOT NULL,
1085
+ name TEXT NOT NULL,
1086
+ parent_scope_id UUID REFERENCES hazo_scopes_l3(id) ON DELETE CASCADE,
1087
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1088
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1089
+ );
1090
+ CREATE INDEX idx_hazo_scopes_l4_org ON hazo_scopes_l4(org);
1091
+ CREATE INDEX idx_hazo_scopes_l4_seq ON hazo_scopes_l4(seq);
1092
+ CREATE INDEX idx_hazo_scopes_l4_parent ON hazo_scopes_l4(parent_scope_id);
1093
+
1094
+ -- Level 5 (parent: L4)
1095
+ CREATE TABLE hazo_scopes_l5 (
1096
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1097
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l5'),
1098
+ org TEXT NOT NULL,
1099
+ name TEXT NOT NULL,
1100
+ parent_scope_id UUID REFERENCES hazo_scopes_l4(id) ON DELETE CASCADE,
1101
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1102
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1103
+ );
1104
+ CREATE INDEX idx_hazo_scopes_l5_org ON hazo_scopes_l5(org);
1105
+ CREATE INDEX idx_hazo_scopes_l5_seq ON hazo_scopes_l5(seq);
1106
+ CREATE INDEX idx_hazo_scopes_l5_parent ON hazo_scopes_l5(parent_scope_id);
1107
+
1108
+ -- Level 6 (parent: L5)
1109
+ CREATE TABLE hazo_scopes_l6 (
1110
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1111
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l6'),
1112
+ org TEXT NOT NULL,
1113
+ name TEXT NOT NULL,
1114
+ parent_scope_id UUID REFERENCES hazo_scopes_l5(id) ON DELETE CASCADE,
1115
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1116
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1117
+ );
1118
+ CREATE INDEX idx_hazo_scopes_l6_org ON hazo_scopes_l6(org);
1119
+ CREATE INDEX idx_hazo_scopes_l6_seq ON hazo_scopes_l6(seq);
1120
+ CREATE INDEX idx_hazo_scopes_l6_parent ON hazo_scopes_l6(parent_scope_id);
1121
+
1122
+ -- Level 7 (parent: L6)
1123
+ CREATE TABLE hazo_scopes_l7 (
1124
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1125
+ seq TEXT NOT NULL DEFAULT hazo_scope_id_generator('hazo_scopes_l7'),
1126
+ org TEXT NOT NULL,
1127
+ name TEXT NOT NULL,
1128
+ parent_scope_id UUID REFERENCES hazo_scopes_l6(id) ON DELETE CASCADE,
1129
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1130
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1131
+ );
1132
+ CREATE INDEX idx_hazo_scopes_l7_org ON hazo_scopes_l7(org);
1133
+ CREATE INDEX idx_hazo_scopes_l7_seq ON hazo_scopes_l7(seq);
1134
+ CREATE INDEX idx_hazo_scopes_l7_parent ON hazo_scopes_l7(parent_scope_id);
1135
+
1136
+ -- 4. Create user scopes junction table
1137
+ CREATE TABLE hazo_user_scopes (
1138
+ user_id UUID NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
1139
+ scope_id UUID NOT NULL,
1140
+ scope_seq TEXT NOT NULL,
1141
+ scope_type hazo_enum_scope_types NOT NULL,
1142
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1143
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1144
+ PRIMARY KEY (user_id, scope_type, scope_id)
1145
+ );
1146
+ CREATE INDEX idx_hazo_user_scopes_user_id ON hazo_user_scopes(user_id);
1147
+ CREATE INDEX idx_hazo_user_scopes_scope_id ON hazo_user_scopes(scope_id);
1148
+ CREATE INDEX idx_hazo_user_scopes_scope_type ON hazo_user_scopes(scope_type);
1149
+
1150
+ -- 5. Create scope labels table
1151
+ CREATE TABLE hazo_scope_labels (
1152
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1153
+ org TEXT NOT NULL,
1154
+ scope_type hazo_enum_scope_types NOT NULL,
1155
+ label TEXT NOT NULL,
1156
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1157
+ changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1158
+ UNIQUE(org, scope_type)
1159
+ );
1160
+ CREATE INDEX idx_hazo_scope_labels_org ON hazo_scope_labels(org);
1161
+ ```
1162
+
1163
+ #### PostgreSQL Grant Scripts
1164
+
1165
+ After creating the tables, grant appropriate permissions:
1166
+
1167
+ ```sql
1168
+ -- Grant to your admin user (replace 'your_admin_user' with actual username)
1169
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l1 TO your_admin_user;
1170
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l2 TO your_admin_user;
1171
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l3 TO your_admin_user;
1172
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l4 TO your_admin_user;
1173
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l5 TO your_admin_user;
1174
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l6 TO your_admin_user;
1175
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l7 TO your_admin_user;
1176
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO your_admin_user;
1177
+ GRANT ALL PRIVILEGES ON TABLE hazo_scope_labels TO your_admin_user;
1178
+ GRANT USAGE ON TYPE hazo_enum_scope_types TO your_admin_user;
1179
+ GRANT EXECUTE ON FUNCTION hazo_scope_id_generator(TEXT) TO your_admin_user;
1180
+
1181
+ -- For PostgREST authenticated role
1182
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l1 TO authenticated;
1183
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l2 TO authenticated;
1184
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l3 TO authenticated;
1185
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l4 TO authenticated;
1186
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l5 TO authenticated;
1187
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l6 TO authenticated;
1188
+ GRANT ALL PRIVILEGES ON TABLE hazo_scopes_l7 TO authenticated;
1189
+ GRANT ALL PRIVILEGES ON TABLE hazo_user_scopes TO authenticated;
1190
+ GRANT ALL PRIVILEGES ON TABLE hazo_scope_labels TO authenticated;
1191
+ GRANT USAGE ON TYPE hazo_enum_scope_types TO authenticated;
1192
+ GRANT EXECUTE ON FUNCTION hazo_scope_id_generator(TEXT) TO authenticated;
1193
+ ```
1194
+
1195
+ #### SQLite
1196
+
1197
+ ```sql
1198
+ -- =============================================
1199
+ -- HRBAC Database Setup Script (SQLite)
1200
+ -- =============================================
1201
+
1202
+ -- Scope tables (L1 through L7)
1203
+
1204
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l1 (
1205
+ id TEXT PRIMARY KEY,
1206
+ seq TEXT NOT NULL,
1207
+ org TEXT NOT NULL,
1208
+ name TEXT NOT NULL,
1209
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1210
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1211
+ );
1212
+
1213
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l2 (
1214
+ id TEXT PRIMARY KEY,
1215
+ seq TEXT NOT NULL,
1216
+ org TEXT NOT NULL,
1217
+ name TEXT NOT NULL,
1218
+ parent_scope_id TEXT REFERENCES hazo_scopes_l1(id) ON DELETE CASCADE,
1219
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1220
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1221
+ );
1222
+
1223
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l3 (
1224
+ id TEXT PRIMARY KEY,
1225
+ seq TEXT NOT NULL,
1226
+ org TEXT NOT NULL,
1227
+ name TEXT NOT NULL,
1228
+ parent_scope_id TEXT REFERENCES hazo_scopes_l2(id) ON DELETE CASCADE,
1229
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1230
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1231
+ );
1232
+
1233
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l4 (
1234
+ id TEXT PRIMARY KEY,
1235
+ seq TEXT NOT NULL,
1236
+ org TEXT NOT NULL,
1237
+ name TEXT NOT NULL,
1238
+ parent_scope_id TEXT REFERENCES hazo_scopes_l3(id) ON DELETE CASCADE,
1239
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1240
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1241
+ );
1242
+
1243
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l5 (
1244
+ id TEXT PRIMARY KEY,
1245
+ seq TEXT NOT NULL,
1246
+ org TEXT NOT NULL,
1247
+ name TEXT NOT NULL,
1248
+ parent_scope_id TEXT REFERENCES hazo_scopes_l4(id) ON DELETE CASCADE,
1249
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1250
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1251
+ );
1252
+
1253
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l6 (
1254
+ id TEXT PRIMARY KEY,
1255
+ seq TEXT NOT NULL,
1256
+ org TEXT NOT NULL,
1257
+ name TEXT NOT NULL,
1258
+ parent_scope_id TEXT REFERENCES hazo_scopes_l5(id) ON DELETE CASCADE,
1259
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1260
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1261
+ );
1262
+
1263
+ CREATE TABLE IF NOT EXISTS hazo_scopes_l7 (
1264
+ id TEXT PRIMARY KEY,
1265
+ seq TEXT NOT NULL,
1266
+ org TEXT NOT NULL,
1267
+ name TEXT NOT NULL,
1268
+ parent_scope_id TEXT REFERENCES hazo_scopes_l6(id) ON DELETE CASCADE,
1269
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1270
+ changed_at TEXT NOT NULL DEFAULT (datetime('now'))
1271
+ );
1272
+
1273
+ -- User scopes junction table
1274
+ CREATE TABLE IF NOT EXISTS hazo_user_scopes (
1275
+ user_id TEXT NOT NULL REFERENCES hazo_users(id) ON DELETE CASCADE,
1276
+ scope_id TEXT NOT NULL,
1277
+ scope_seq TEXT NOT NULL,
1278
+ scope_type TEXT NOT NULL CHECK(scope_type IN ('hazo_scopes_l1','hazo_scopes_l2','hazo_scopes_l3','hazo_scopes_l4','hazo_scopes_l5','hazo_scopes_l6','hazo_scopes_l7')),
1279
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1280
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1281
+ PRIMARY KEY (user_id, scope_type, scope_id)
1282
+ );
1283
+
1284
+ -- Scope labels table
1285
+ CREATE TABLE IF NOT EXISTS hazo_scope_labels (
1286
+ id TEXT PRIMARY KEY,
1287
+ org TEXT NOT NULL,
1288
+ scope_type TEXT NOT NULL CHECK(scope_type IN ('hazo_scopes_l1','hazo_scopes_l2','hazo_scopes_l3','hazo_scopes_l4','hazo_scopes_l5','hazo_scopes_l6','hazo_scopes_l7')),
1289
+ label TEXT NOT NULL,
1290
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1291
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
1292
+ UNIQUE(org, scope_type)
1293
+ );
1294
+ ```
1295
+
1296
+ ### Step 7.4: Verify HRBAC Tables
1297
+
1298
+ #### PostgreSQL
1299
+ ```sql
1300
+ SELECT table_name FROM information_schema.tables
1301
+ WHERE table_name LIKE 'hazo_scopes_%'
1302
+ OR table_name IN ('hazo_user_scopes', 'hazo_scope_labels');
1303
+ -- Expected: 9 tables (7 scope tables + user_scopes + scope_labels)
1304
+ ```
1305
+
1306
+ #### SQLite
1307
+ ```bash
1308
+ sqlite3 data/hazo_auth.sqlite ".tables" | grep -E "hazo_scopes|hazo_user_scopes|hazo_scope_labels"
1309
+ ```
1310
+
1311
+ ### Step 7.5: Test HRBAC
1312
+
1313
+ 1. Start your dev server: `npm run dev`
1314
+ 2. Log in with a user that has `admin_scope_hierarchy_management` permission
1315
+ 3. Visit `/hazo_auth/user_management`
1316
+ 4. Verify the "Scope Hierarchy", "Scope Labels", and "User Scopes" tabs appear
1317
+ 5. Visit `/hazo_auth/scope_test` to test scope access checking
1318
+
1319
+ **HRBAC Checklist:**
1320
+ - [ ] `enable_hrbac = true` in config
1321
+ - [ ] HRBAC permissions added to defaults
1322
+ - [ ] All 9 HRBAC tables created
1323
+ - [ ] Grants applied (PostgreSQL)
1324
+ - [ ] HRBAC tabs visible in User Management
1325
+ - [ ] Scope test page works
1326
+
1327
+ ---
1328
+
960
1329
  ## Troubleshooting
961
1330
 
962
1331
  ### Issue: Email not sending
@@ -1 +1 @@
1
- {"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,2CAwQ/B"}
1
+ {"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,2CAyQ/B"}
@@ -128,9 +128,9 @@ export function ProfilePictureLibraryTab({ useLibrary, onUseLibraryChange, onPho
128
128
  };
129
129
  return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-[var(--hazo-text-muted)]", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: `cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] max-h-[400px]`, children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
130
130
  cls_profile_picture_library_tab_photo_thumbnail
131
- aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
131
+ w-full aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
132
132
  ${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-[var(--hazo-border)] hover:border-[var(--hazo-border-emphasis)]"}
133
- `, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
133
+ `, style: { minHeight: '80px', minWidth: '80px' }, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
134
134
  // Fallback if image fails to load
135
135
  const target = e.target;
136
136
  target.style.display = 'none';
@@ -0,0 +1,15 @@
1
+ export type RbacTestLayoutProps = {
2
+ className?: string;
3
+ /** Whether HRBAC is enabled (passed from server) */
4
+ hrbacEnabled?: boolean;
5
+ /** Default organization for HRBAC scopes */
6
+ defaultOrg?: string;
7
+ };
8
+ /**
9
+ * RBAC/HRBAC Test layout component
10
+ * Allows testing permissions and scope access for different users
11
+ * @param props - Component props
12
+ * @returns RBAC test layout component
13
+ */
14
+ export declare function RbacTestLayout({ className, hrbacEnabled, defaultOrg, }: RbacTestLayoutProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/rbac_test/index.tsx"],"names":[],"mappings":"AA2CA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA+GF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,YAAoB,EACpB,UAAe,GAChB,EAAE,mBAAmB,2CAw3BrB"}