# RBAC inventory from current code and DB Source date: 2026-05-14. This document is the baseline for RBAC migration. The first migration must preserve the current effective access of the five existing roles. ## DB role snapshot Collected from `users.role` with soft-deleted users included. | Role | Total users | Active | Deleted | |------|-------------|--------|---------| | `admin` | 4 | 3 | 1 | | `assistant_head` | 2 | 2 | 0 | | `brigadier` | 9 | 7 | 2 | | `manager` | 5 | 4 | 1 | | `warehouse_head` | 2 | 2 | 0 | Role deletion rule for new RBAC: a role cannot be deleted while any `users.role_id` points to it, including soft-deleted users. ## Current role semantics Current role checks are not a simple equality check. `Role::effectiveRoles()` currently expands: ```text assistant_head -> assistant_head, admin, manager all other roles -> themselves ``` Consequences: - `assistant_head` passes `hasRole('admin')`. - `assistant_head` passes route middleware `role:admin`. - `assistant_head` passes `hasRole('admin,manager')`. - direct DB queries like `where('role', Role::ADMIN)` do not include `assistant_head`. During RBAC migration `assistant_head` must receive its own explicit permissions equivalent to current route/helper access. Do not implement role inheritance. ## User-facing modules found in controllers/routes | Module key | Current table id / controller source | Main controller(s) | |------------|--------------------------------------|--------------------| | `orders` | `orders` | `OrderController` | | `reclamations` | `reclamations` | `ReclamationController` | | `schedule` | `schedule` | `ScheduleController` | | `catalog` | `products` | `ProductController` | | `maf` | `product_sku` | `ProductSKUController` | | `maf_orders` | `maf_order` | `MafOrderController` | | `contracts` | `contracts` | `ContractController` | | `contractors` | controller-level `$id` | `ContractorController` | | `responsibles` | `responsibles` | `ResponsibleController` | | `reports` | `reports` | `ReportController` | | `users` | `users` | `UserController` | | `profile` | no table id | `UserController` | | `import` | `import` | `ImportController` | | `admin_settings` | no table id | `AdminSettingsController` | | `districts` | controller-level `$id` | `AdminDistrictController` | | `areas` | controller-level `$id`, ajax `areas` route | `AdminAreaController`, `AreaController` | | `clear_data` | no table id | `ClearDataController` | | `year_data` | no table id | `YearDataController` | | `notifications` | `notifications` | `UserNotificationController` | | `notification_logs` | `notification_logs` | `AdminNotificationLogController` | | `spare_parts` | `spare_parts` | `SparePartController` | | `spare_part_orders` | `spare_part_orders` | `SparePartOrderController` | | `spare_part_reservations` | ajax/list endpoints | `SparePartReservationController` | | `spare_part_inventory` | `spare_part_inventory` | `SparePartInventoryController` | | `pricing_codes` | `pricing_codes` | `PricingCodeController` | | `chat_messages` | no table id | `ChatMessageController` | | `filters` | no table id | `FilterController` | ## Current route/module access baseline Legend: - `auth` means any authenticated user. - `admin` includes `assistant_head` through `effectiveRoles()` when checked through `hasRole()` or `role:*`. - `object-level` means additional record-level filtering/checking exists and must remain in Policies/scopes. | Module | Current access from routes/code | RBAC module permissions to seed | |--------|---------------------------------|---------------------------------| | `orders` | `index`, `show`, photo upload, photo pack, tech docs, chat create: `auth`. Edit/store/update/document upload/statement upload/photo/document/statement delete/document generation/reports group: `admin,manager`. Delete, create, export-one, MAF attach/revert/move, TTN, bulk photo/document/statement delete: `admin`. Contractor specification: `admin,assistant_head`. Brigadier and warehouse have object-level filtering in controller. | `orders.view`, `orders.create`, `orders.update`, `orders.delete`, `orders.export`, `orders.photos.upload`, `orders.photos.delete`, `orders.documents.upload`, `orders.documents.delete`, `orders.documents.generate`, `orders.maf.manage`, `orders.ttn.create`, `orders.contractor_specification.create`, `orders.chat.create`, `orders.chat.delete` | | `reclamations` | `index`, `show`, photo before/after upload, chat create: `auth`. Create/update/status/document upload/details/spare parts/delete photos/generate packs: `admin,manager`. Upload act: `admin,manager,brigadier,warehouse_head`. Delete act/document/export: `admin,manager`. Delete reclamation/chat message: `admin`. Brigadier and warehouse have object-level filtering/checking in controller. | `reclamations.view`, `reclamations.create`, `reclamations.update`, `reclamations.delete`, `reclamations.export`, `reclamations.status.update`, `reclamations.documents.upload`, `reclamations.documents.delete`, `reclamations.documents.generate`, `reclamations.photos.upload`, `reclamations.photos.delete`, `reclamations.act.upload`, `reclamations.act.delete`, `reclamations.spare_parts.manage`, `reclamations.chat.create`, `reclamations.chat.delete` | | `schedule` | Index: `auth`. Create from order/reclamation, update, export, delete: `admin`. Brigadier sees own records through object-level filtering and has controller checks for some actions. | `schedule.view`, `schedule.create`, `schedule.update`, `schedule.delete`, `schedule.export` | | `catalog` | Index/show: `admin,manager`. Create/store/update/delete/export/delete certificate: `admin`. Upload certificate/thumbnail currently in `admin,manager` route group, but edit UI shows upload only for `admin`; seed as `admin` to match visible UI or audit before migration. Product search endpoint: `auth`. | `catalog.view`, `catalog.create`, `catalog.update`, `catalog.delete`, `catalog.export`, `catalog.certificates.upload`, `catalog.certificates.delete`, `catalog.thumbnail.upload`, `catalog.search` | | `maf` | Product SKU index/show/update/upload passport: `admin,manager`. Import/export/delete passport: `admin`. Blade edit fields mostly admin-only. | `maf.view`, `maf.update`, `maf.import`, `maf.export`, `maf.passports.upload`, `maf.passports.delete` | | `maf_orders` | All routes are inside `role:admin`, therefore `admin` and `assistant_head` currently pass middleware. Blade also uses `hasRole('admin,assistant_head')`. | `maf_orders.view`, `maf_orders.create`, `maf_orders.update`, `maf_orders.delete`, `maf_orders.stock.manage` | | `contracts` | Routes are in `role:admin,manager` group, but `StoreContractRequest` currently authorizes only `admin`. Effective write behavior is stricter than routes. | `contracts.view`, `contracts.create`, `contracts.update`, `contracts.delete` | | `contractors` | Routes are inside parent `admin,manager` group and child `role:admin,assistant_head`. Effective access: `admin` and `assistant_head`; `manager` does not pass child middleware. | `contractors.view`, `contractors.create`, `contractors.update`, `contractors.delete`, `contractors.prices.update`, `contractors.prices.import`, `contractors.prices.export` | | `responsibles` | Routes and FormRequest: `admin,manager`. | `responsibles.view`, `responsibles.create`, `responsibles.update`, `responsibles.delete` | | `reports` | Route: `admin,manager`. | `reports.view` | | `users` | Admin route group. Create/update/delete/restore/impersonate: `admin` through middleware/FormRequest. | `users.view`, `users.create`, `users.update`, `users.delete`, `users.restore`, `users.impersonate`, `users.permissions.manage` | | `profile` | View/update/delete own profile: `auth`; object-level own-user rules. | `profile.view`, `profile.update`, `profile.delete` | | `import` | Admin route group. | `import.view`, `import.create` | | `admin_settings` | Admin route group. | `admin.settings.view`, `admin.settings.update` | | `districts` | Admin route group with CRUD/import/export/restore. | `districts.view`, `districts.create`, `districts.update`, `districts.delete`, `districts.restore`, `districts.import`, `districts.export` | | `areas` | Admin dictionary route group with CRUD/import/export/restore. Public ajax `areas/{district_id?}` is `auth`. | `areas.view`, `areas.create`, `areas.update`, `areas.delete`, `areas.restore`, `areas.import`, `areas.export`, `areas.ajax.view` | | `clear_data` | Admin route group. | `admin.clear_data.view`, `admin.clear_data.delete` | | `year_data` | Admin route group with import/export/download/stats. | `admin.year_data.view`, `admin.year_data.import`, `admin.year_data.export`, `admin.year_data.download` | | `notifications` | User notifications: `auth`. | `notifications.view`, `notifications.mark_read` | | `notification_logs` | Admin route group. | `admin.notification_logs.view` | | `spare_parts` | Index/help/search/show: `admin,manager`. Create/store/update/delete/export/import/upload image: `admin`. Some edit fields readonly for non-admin/non-manager. | `spare_parts.view`, `spare_parts.create`, `spare_parts.update`, `spare_parts.delete`, `spare_parts.import`, `spare_parts.export`, `spare_parts.image.upload`, `spare_parts.search` | | `spare_part_orders` | Index/create/show/store/update/ship/set stock: `admin,manager`. Delete/correct: `admin`. | `spare_part_orders.view`, `spare_part_orders.create`, `spare_part_orders.update`, `spare_part_orders.delete`, `spare_part_orders.ship`, `spare_part_orders.stock.manage`, `spare_part_orders.correct` | | `spare_part_reservations` | All reservation routes: `admin,manager`. | `spare_part_reservations.view`, `spare_part_reservations.manage`, `spare_part_reservations.issue`, `spare_part_reservations.cancel`, `spare_part_reservations.reassign` | | `spare_part_inventory` | Route: `admin,manager`. | `spare_part_inventory.view` | | `pricing_codes` | CRUD routes: `admin`. `get-description` and `search` are currently `auth` without role restriction. | `pricing_codes.view`, `pricing_codes.create`, `pricing_codes.update`, `pricing_codes.delete`, `pricing_codes.search` | | `chat_messages` | Create for orders/reclamations: `auth` plus object-level checks. Delete: `admin`. Notifications from chat UI: `admin,manager`. | `chat_messages.create`, `chat_messages.delete`, `chat_messages.notify` | | `filters` | `get-filters`: `auth`; must be filtered by field visibility. | `filters.view` | ## Seed matrix for built-in roles The seeder must create these role permissions first, then manual RBAC changes can adjust them later. | Role | Seed rule | |------|-----------| | `admin` | All action permissions allow. All field `view` and `update` permissions allow. Deny is forbidden. | | `assistant_head` | Materialize current `effectiveRoles()` behavior: all permissions that `admin` and `manager` receive through `role:*`/`hasRole()`, plus direct `assistant_head` checks. Do not create runtime inheritance. | | `manager` | All permissions currently guarded by `admin,manager`, plus all `auth` permissions that are available to every authenticated user. Do not include admin-only permissions. | | `brigadier` | All `auth` permissions available to every authenticated user, plus `reclamations.act.upload`; preserve object-level restrictions for own orders/reclamations/schedule. | | `warehouse_head` | All `auth` permissions available to every authenticated user, plus `reclamations.act.upload`; preserve current warehouse object-level filtering in orders/reclamations/chat controllers. | ## Field ACL policy decisions User decisions for implementation: - Field permissions use only `view` and `update`. - Export and import are controlled by action permissions. If a user has export/import action access, the export/import may include the whole document/data set for that action. - Document access is all-or-nothing. If a user has document permission, return the full document without field masking. - Hidden required fields are not validated as required for that user. - Readonly fields may be displayed, but backend must remove them from update payload. - If create requires a field the user cannot set, deny create unless a safe default exists. - Hidden fields must also be removed from filters, sorting, and search server-side. - `assistant_head` keeps current effective access on migration; permissions can be manually reduced later. - Roles cannot be deleted while any user, including soft-deleted users, references the role. - Permissions are cached; invalidate cache on role, permission, role assignment, and user permission changes. - A baseline test must be added before/with migration to prove built-in roles keep current access. ## Current DB/user migration notes - Existing `users.role` values are valid and match the five system roles. - `users.role_id` can be backfilled directly by `roles.slug = users.role`. - Soft-deleted users must also receive `role_id`. - Role deletion checks must include soft-deleted users. ## Known inconsistencies to preserve initially These are not blockers, but they must not be "fixed" accidentally during the migration: - `assistant_head` passes `role:admin`, but direct `where('role', Role::ADMIN)` queries do not include it. - `contracts` routes allow `admin,manager`, but `StoreContractRequest` allows only `admin`. - `catalog.upload-certificate` and `catalog.upload-thumbnail` routes are in `admin,manager` group, while Blade only exposes controls to `admin`. - Some FormRequests use `auth()->check()` while routes are stricter. Keep route-level effective behavior during migration. - `pricing-codes/get-description` and `pricing-codes/search` are role-unrestricted inside auth group even though CRUD is admin-only.