|
|
@@ -1,104 +1,467 @@
|
|
|
-# План внедрения гибкой ролевой модели (RBAC)
|
|
|
+# План внедрения гибкой ролевой модели RBAC + Field-Level ACL
|
|
|
|
|
|
-## Обзор
|
|
|
+## Цель
|
|
|
|
|
|
-Внедрение системы RBAC (Role-Based Access Control) для Stroyprofit CRM с возможностью создания новых ролей и назначения им прав через UI.
|
|
|
+Внедрить гибкую модель доступа для Stroyprofit CRM:
|
|
|
|
|
|
-## Текущее состояние
|
|
|
+- роли создаются и настраиваются через UI;
|
|
|
+- пользователю можно назначить роль и индивидуальные переопределения;
|
|
|
+- доступ проверяется не только к сущности/действию, но и к конкретному полю сущности;
|
|
|
+- UI, контроллеры, FormRequest, импорт, экспорт, ajax и сервисы используют единый механизм доступа;
|
|
|
+- существующие проверки `hasRole()` и middleware `role:*` продолжают работать во время миграции.
|
|
|
|
|
|
-| Компонент | Описание |
|
|
|
-|-----------|----------|
|
|
|
-| Хранение ролей | Поле `role` (string) в таблице `users` |
|
|
|
-| Класс Role | Статический класс с константами `app/Models/Role.php` |
|
|
|
-| Проверка ролей | Helper функция `hasRole()` в `app/Helpers/roles.php` |
|
|
|
-| Middleware | `EnsureUserHasRole` - проверка через `in_array()` |
|
|
|
-| Точки проверки | 155+ вызовов `hasRole()` в Blade, контроллерах, FormRequest |
|
|
|
+Примеры целевого поведения:
|
|
|
|
|
|
-**Текущие роли:** admin, manager, brigadier
|
|
|
+- показать каталог, но скрыть цены;
|
|
|
+- показать каталог, разрешить редактировать продукт, но запретить менять `name_tz`;
|
|
|
+- разрешить менеджеру видеть рекламации, но скрыть часть полей или запретить конкретные действия;
|
|
|
+- дать конкретному пользователю дополнительное право без создания отдельной роли;
|
|
|
+- явно запретить пользователю право, даже если оно есть у роли.
|
|
|
+
|
|
|
+Baseline текущего доступа из кода и БД зафиксирован в `docs/flex_roles_access_inventory.md`. Реализация должна использовать его как источник стартовой матрицы permissions для системных ролей.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Текущее состояние кода
|
|
|
+
|
|
|
+| Компонент | Текущее состояние |
|
|
|
+|-----------|-------------------|
|
|
|
+| Хранение роли | `users.role` string в таблице `users` |
|
|
|
+| Роли | `app/Models/Role.php` - статический класс с константами |
|
|
|
+| Текущие роли | `admin`, `manager`, `brigadier`, `warehouse_head`, `assistant_head` |
|
|
|
+| Legacy-наследование ролей | `Role::effectiveRoles()`: `assistant_head` сейчас получает `assistant_head`, `admin`, `manager` |
|
|
|
+| Helper | `app/Helpers/roles.php`: `getRoles()`, `hasRole()`, `roleName()` |
|
|
|
+| Middleware | `EnsureUserHasRole`, alias `role` |
|
|
|
+| Проверки | `hasRole()` в Blade, контроллерах, FormRequest; role middleware в `routes/web.php` |
|
|
|
+| Прямые запросы | Есть `User::where('role', ...)` / `whereIn('role', ...)` в уведомлениях, чатах, контроллерах |
|
|
|
+| UI таблиц | Общий `resources/views/partials/table.blade.php` выводит поля из `$header` без field ACL |
|
|
|
+| Каталог | Цены выводятся через `product_price_txt`, `installation_price_txt`, `total_price_txt`; редактирование сейчас в основном ограничено `admin` |
|
|
|
+
|
|
|
+Важно: целевая модель не должна использовать наследование ролей. Но стартовая матрица прав должна сохранить фактические права всех базовых ролей такими же, как сейчас, включая текущее поведение `assistant_head`.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Основные принципы
|
|
|
+
|
|
|
+1. **Deny имеет приоритет над allow.** Если пользователю или роли явно запрещено право, оно запрещено даже при наличии другого разрешения.
|
|
|
+2. **User override сильнее role permissions.** Индивидуальные настройки пользователя применяются поверх роли.
|
|
|
+3. **Admin получает все права по умолчанию**, но системные критичные действия всё равно должны иметь защиту от удаления последнего администратора и самоблокировки.
|
|
|
+4. **Проверка на сервере обязательна.** Скрытие кнопки или поля в Blade не является защитой.
|
|
|
+5. **Полевая модель применяется ко всем каналам:** Blade, таблицы, формы, POST/PUT payload, импорт, экспорт, ajax, генерация документов, API/JSON.
|
|
|
+6. **Права должны быть сгруппированы для UI.** Настройка ролей не должна быть плоским списком из сотен чекбоксов.
|
|
|
+7. **Обратная совместимость нужна на переходный период.** `hasRole()`, `role:*`, `Role::ADMIN` и старые Blade-проверки нельзя ломать одним шагом.
|
|
|
+8. **Наследование ролей не используется в целевой модели.** Для похожих наборов прав используется копирование роли с дальнейшим независимым редактированием.
|
|
|
+9. **Роль admin неизменяемая по разрешениям.** У администратора всегда все permissions allow; убирать разрешения, ставить deny или индивидуально запрещать права admin-пользователю нельзя.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 1: База данных
|
|
|
+## Термины
|
|
|
|
|
|
-### Миграции
|
|
|
+| Термин | Пример | Назначение |
|
|
|
+|--------|--------|------------|
|
|
|
+| Module | `catalog`, `orders` | Логический модуль CRM |
|
|
|
+| Entity | `product`, `order` | Сущность доменной модели |
|
|
|
+| Action permission | `catalog.view`, `catalog.update` | Доступ к действию над модулем/сущностью |
|
|
|
+| Field permission | `catalog.fields.product_price.view` | Доступ к полю |
|
|
|
+| Scope | `view`, `create`, `update`, `delete`, `export`, `import` | Тип действия |
|
|
|
+| Effect | `allow`, `deny` | Разрешение или явный запрет |
|
|
|
+| Role override | Настройки роли | Базовая матрица доступа |
|
|
|
+| User override | Настройки пользователя | Исключения для конкретного пользователя |
|
|
|
|
|
|
-**1.1 Таблица `roles`**
|
|
|
+---
|
|
|
+
|
|
|
+## Архитектура доступа
|
|
|
+
|
|
|
+### 1. Action permissions
|
|
|
+
|
|
|
+Права уровня модуля/сущности:
|
|
|
+
|
|
|
+```text
|
|
|
+catalog.view
|
|
|
+catalog.create
|
|
|
+catalog.update
|
|
|
+catalog.delete
|
|
|
+catalog.import
|
|
|
+catalog.export
|
|
|
+catalog.certificates.upload
|
|
|
+catalog.certificates.delete
|
|
|
+orders.view
|
|
|
+orders.create
|
|
|
+orders.update
|
|
|
+orders.delete
|
|
|
+orders.photos.upload
|
|
|
+orders.photos.delete
|
|
|
+orders.documents.generate
|
|
|
```
|
|
|
-database/migrations/2026_02_03_000001_create_roles_table.php
|
|
|
+
|
|
|
+Action permission отвечает за доступ к маршруту или операции в целом.
|
|
|
+
|
|
|
+### 2. Field permissions
|
|
|
+
|
|
|
+Права уровня поля:
|
|
|
+
|
|
|
+```text
|
|
|
+catalog.fields.article.view
|
|
|
+catalog.fields.article.update
|
|
|
+catalog.fields.name_tz.view
|
|
|
+catalog.fields.name_tz.update
|
|
|
+catalog.fields.product_price.view
|
|
|
+catalog.fields.product_price.update
|
|
|
+catalog.fields.installation_price.view
|
|
|
+catalog.fields.installation_price.update
|
|
|
+catalog.fields.total_price.view
|
|
|
+catalog.fields.total_price.update
|
|
|
```
|
|
|
-- `id`, `slug` (unique), `name`, `description`, `is_system` (boolean), `timestamps`
|
|
|
|
|
|
-**1.2 Таблица `permissions`**
|
|
|
+Field permission отвечает за:
|
|
|
+
|
|
|
+- отображение поля в таблице;
|
|
|
+- отображение поля в карточке/форме;
|
|
|
+- возможность редактировать поле;
|
|
|
+- участие поля в фильтрах, сортировке и поиске;
|
|
|
+- разрешение обновления поля при POST/PUT.
|
|
|
+
|
|
|
+Field-level ACL использует только `view` и `update`. Экспорт, импорт и документы регулируются action permissions: если пользователь получил доступ к экспорту/импорту/документу, он получает полный результат этой операции без field masking.
|
|
|
+
|
|
|
+### 3. Effective permissions
|
|
|
+
|
|
|
+Итоговые права пользователя вычисляются так:
|
|
|
+
|
|
|
+1. системная роль admin -> all allow, кроме явных защит бизнес-логики;
|
|
|
+2. права роли;
|
|
|
+3. индивидуальные `user_permissions`;
|
|
|
+4. индивидуальные deny перекрывают allow;
|
|
|
+5. role deny перекрывает role allow;
|
|
|
+6. отсутствие allow означает запрет.
|
|
|
+
|
|
|
+Для производительности итоговые права можно кэшировать по ключу:
|
|
|
+
|
|
|
+```text
|
|
|
+permissions:user:{id}:v{updated_at_hash}
|
|
|
```
|
|
|
-database/migrations/2026_02_03_000002_create_permissions_table.php
|
|
|
+
|
|
|
+Permissions нужно кэшировать. Сброс кэша обязателен при:
|
|
|
+
|
|
|
+- изменении роли;
|
|
|
+- изменении `role_permissions`;
|
|
|
+- изменении `user_permissions`;
|
|
|
+- изменении роли пользователя (`users.role_id` или legacy `users.role`);
|
|
|
+- изменении системного справочника permissions;
|
|
|
+- запуске сидера RBAC.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## База данных
|
|
|
+
|
|
|
+### 1. `roles`
|
|
|
+
|
|
|
+```text
|
|
|
+id
|
|
|
+slug unique
|
|
|
+name
|
|
|
+description nullable
|
|
|
+is_system boolean default false
|
|
|
+is_active boolean default true
|
|
|
+sort integer default 100
|
|
|
+timestamps
|
|
|
```
|
|
|
-- `id`, `slug` (unique), `name`, `module`, `action`, `description`, `timestamps`
|
|
|
|
|
|
-**1.3 Связь ролей и прав `role_has_permissions`**
|
|
|
+Системные роли при миграции:
|
|
|
+
|
|
|
+- `admin`
|
|
|
+- `manager`
|
|
|
+- `brigadier`
|
|
|
+- `warehouse_head`
|
|
|
+- `assistant_head`
|
|
|
+
|
|
|
+### 2. `permissions`
|
|
|
+
|
|
|
+```text
|
|
|
+id
|
|
|
+slug unique
|
|
|
+name
|
|
|
+description nullable
|
|
|
+module
|
|
|
+entity nullable
|
|
|
+field nullable
|
|
|
+action
|
|
|
+type enum: action, field
|
|
|
+group nullable
|
|
|
+sort integer default 100
|
|
|
+is_system boolean default true
|
|
|
+timestamps
|
|
|
```
|
|
|
-database/migrations/2026_02_03_000003_create_role_has_permissions_table.php
|
|
|
+
|
|
|
+Примеры:
|
|
|
+
|
|
|
+| slug | module | entity | field | action | type | group |
|
|
|
+|------|--------|--------|-------|--------|------|-------|
|
|
|
+| `catalog.view` | catalog | product | null | view | action | Каталог |
|
|
|
+| `catalog.update` | catalog | product | null | update | action | Каталог |
|
|
|
+| `catalog.fields.product_price.view` | catalog | product | product_price | view | field | Каталог / Поля |
|
|
|
+| `catalog.fields.product_price.update` | catalog | product | product_price | update | field | Каталог / Поля |
|
|
|
+
|
|
|
+### 3. `role_permissions`
|
|
|
+
|
|
|
+```text
|
|
|
+role_id foreign
|
|
|
+permission_id foreign
|
|
|
+effect enum: allow, deny
|
|
|
+timestamps
|
|
|
+primary(role_id, permission_id)
|
|
|
```
|
|
|
-- `role_id` (FK), `permission_id` (FK), составной PK
|
|
|
|
|
|
-**1.4 Добавление `role_id` в `users`**
|
|
|
+`effect` нужен, чтобы роль могла иметь явный запрет, а не только отсутствие разрешения.
|
|
|
+
|
|
|
+### 4. `user_permissions`
|
|
|
+
|
|
|
+```text
|
|
|
+user_id foreign
|
|
|
+permission_id foreign
|
|
|
+effect enum: allow, deny
|
|
|
+reason nullable
|
|
|
+expires_at nullable
|
|
|
+timestamps
|
|
|
+primary(user_id, permission_id)
|
|
|
```
|
|
|
-database/migrations/2026_02_03_000004_add_role_id_to_users_table.php
|
|
|
+
|
|
|
+`expires_at` необязателен, но полезен для временного доступа.
|
|
|
+
|
|
|
+### 5. `users.role_id`
|
|
|
+
|
|
|
+Добавить:
|
|
|
+
|
|
|
+```text
|
|
|
+role_id foreign nullable
|
|
|
```
|
|
|
-- `role_id` (FK nullable) - связь с таблицей roles
|
|
|
+
|
|
|
+На переходный период оставить `users.role`.
|
|
|
+
|
|
|
+Миграция:
|
|
|
+
|
|
|
+1. создать роли по текущим slug;
|
|
|
+2. заполнить `users.role_id` по `users.role`;
|
|
|
+3. сохранить `users.role` для обратной совместимости;
|
|
|
+4. позже, отдельной фазой, удалить зависимость кода от `users.role`.
|
|
|
+
|
|
|
+### 6. Наследование ролей
|
|
|
+
|
|
|
+Таблица наследования ролей не создаётся.
|
|
|
+
|
|
|
+Причины:
|
|
|
+
|
|
|
+- источник права становится менее очевидным для администратора;
|
|
|
+- `allow/deny` на нескольких уровнях сложнее объяснять и тестировать;
|
|
|
+- при field-level ACL количество комбинаций резко растёт;
|
|
|
+- тот же результат проще получить копированием роли.
|
|
|
+
|
|
|
+В UI нужно добавить действия "Скопировать роль" и "Создать роль из пользователя". Новая роль получает независимую копию разрешений исходной роли или итоговых разрешений выбранного пользователя, после чего редактируется отдельно.
|
|
|
+
|
|
|
+Текущее поведение `Role::effectiveRoles()` используется только как источник для миграции стартовой матрицы. После миграции у `assistant_head` должен быть собственный полный набор permissions, эквивалентный текущему фактическому доступу, без runtime-наследования `admin` и `manager`.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 2: Модели
|
|
|
+## Модели и сервисы
|
|
|
+
|
|
|
+### `app/Models/Role.php`
|
|
|
+
|
|
|
+Преобразовать в Eloquent model, но сохранить:
|
|
|
+
|
|
|
+- `ADMIN`
|
|
|
+- `MANAGER`
|
|
|
+- `BRIGADIER`
|
|
|
+- `WAREHOUSE_HEAD`
|
|
|
+- `ASSISTANT_HEAD`
|
|
|
+- `VALID_ROLES`
|
|
|
+- `NAMES`
|
|
|
+- `effectiveRoles()`
|
|
|
+
|
|
|
+`effectiveRoles()` оставить только для legacy fallback и проверки обратной совместимости. Новая логика доступа не должна строиться на наследовании ролей.
|
|
|
+
|
|
|
+Связи:
|
|
|
+
|
|
|
+```php
|
|
|
+permissions(): BelongsToMany
|
|
|
+users(): HasMany
|
|
|
+```
|
|
|
+
|
|
|
+Методы:
|
|
|
+
|
|
|
+```php
|
|
|
+hasPermission(string $permission): bool
|
|
|
+givePermission(string $permission, string $effect = 'allow'): void
|
|
|
+syncPermissions(array $permissions): void
|
|
|
+```
|
|
|
+
|
|
|
+### `app/Models/Permission.php`
|
|
|
|
|
|
-### 2.1 Обновление `app/Models/Role.php`
|
|
|
+Связи:
|
|
|
|
|
|
-Преобразовать из статического класса в Eloquent модель:
|
|
|
-- Сохранить константы для обратной совместимости (ADMIN, MANAGER, BRIGADIER)
|
|
|
-- Добавить связь `permissions()` (belongsToMany)
|
|
|
-- Добавить связь `users()` (hasMany)
|
|
|
-- Методы: `hasPermission()`, `givePermissions()`, `syncPermissions()`
|
|
|
+```php
|
|
|
+roles(): BelongsToMany
|
|
|
+users(): BelongsToMany
|
|
|
+```
|
|
|
|
|
|
-### 2.2 Новая модель `app/Models/Permission.php`
|
|
|
+Методы:
|
|
|
|
|
|
-- Связь `roles()` (belongsToMany)
|
|
|
-- Статический метод `getGroupedByModule()`
|
|
|
+```php
|
|
|
+getGroupedForUi(): Collection
|
|
|
+actionPermissions(): Builder
|
|
|
+fieldPermissions(): Builder
|
|
|
+```
|
|
|
|
|
|
-### 2.3 Обновление `app/Models/User.php`
|
|
|
+### `app/Models/User.php`
|
|
|
|
|
|
Добавить:
|
|
|
-- Поле `role_id` в `$fillable`
|
|
|
-- Связь `roleModel()` (belongsTo Role)
|
|
|
-- Метод `hasRole()` - сначала проверяет `role_id`, fallback на `role`
|
|
|
-- Метод `hasPermission()` - админ имеет все права
|
|
|
-- Метод `hasAnyPermission()`
|
|
|
-- Метод `getAllPermissions()`
|
|
|
+
|
|
|
+```php
|
|
|
+role_id
|
|
|
+roleModel(): BelongsTo
|
|
|
+permissions(): BelongsToMany
|
|
|
+hasRole(string|array $roles): bool
|
|
|
+hasPermission(string $permission): bool
|
|
|
+hasAnyPermission(array|string $permissions): bool
|
|
|
+canViewField(string $module, string $field, ?string $entity = null): bool
|
|
|
+canUpdateField(string $module, string $field, ?string $entity = null): bool
|
|
|
+getEffectivePermissions(): Collection
|
|
|
+```
|
|
|
+
|
|
|
+`hasRole()` на переходный период:
|
|
|
+
|
|
|
+1. если есть `role_id`, проверять slug роли из БД;
|
|
|
+2. fallback на `users.role`;
|
|
|
+3. учитывать `Role::effectiveRoles()`.
|
|
|
+
|
|
|
+### `app/Services/Access/AccessService.php`
|
|
|
+
|
|
|
+Единая точка принятия решений:
|
|
|
+
|
|
|
+```php
|
|
|
+can(User $user, string $permission): bool
|
|
|
+canAny(User $user, array $permissions): bool
|
|
|
+canViewField(User $user, string $module, string $field, ?string $entity = null): bool
|
|
|
+canUpdateField(User $user, string $module, string $field, ?string $entity = null): bool
|
|
|
+filterReadableFields(User $user, string $module, array $fields, ?string $entity = null): array
|
|
|
+filterWritableData(User $user, string $module, array $data, ?string $entity = null): array
|
|
|
+assertCan(User $user, string $permission): void
|
|
|
+```
|
|
|
+
|
|
|
+Вся логика allow/deny/user override/cache должна жить здесь, а не размазываться по Blade и контроллерам.
|
|
|
+
|
|
|
+### `app/Services/Access/FieldAccessService.php`
|
|
|
+
|
|
|
+Можно выделить отдельно, если `AccessService` станет слишком большим:
|
|
|
+
|
|
|
+```php
|
|
|
+visibleHeaders(User $user, string $module, array $headers): array
|
|
|
+filterExportColumns(User $user, string $module, array $columns): array
|
|
|
+filterImportPayload(User $user, string $module, array $row): array
|
|
|
+filterValidatedPayload(User $user, string $module, array $validated): array
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Helpers и Blade
|
|
|
+
|
|
|
+### `app/Helpers/roles.php`
|
|
|
+
|
|
|
+Обновить и расширить:
|
|
|
+
|
|
|
+```php
|
|
|
+getRoles($key = null)
|
|
|
+hasRole($roles, $user = null): bool
|
|
|
+roleName($role): string
|
|
|
+hasPermission(string $permission, $user = null): bool
|
|
|
+hasAnyPermission(array|string $permissions, $user = null): bool
|
|
|
+canViewField(string $module, string $field, ?string $entity = null, $user = null): bool
|
|
|
+canUpdateField(string $module, string $field, ?string $entity = null, $user = null): bool
|
|
|
+```
|
|
|
+
|
|
|
+### Blade directives
|
|
|
+
|
|
|
+В `AppServiceProvider`:
|
|
|
+
|
|
|
+```php
|
|
|
+Blade::if('role', fn($roles) => hasRole($roles));
|
|
|
+Blade::if('permission', fn($permission) => hasPermission($permission));
|
|
|
+Blade::if('anypermission', fn($permissions) => hasAnyPermission($permissions));
|
|
|
+Blade::if('fieldView', fn($module, $field, $entity = null) => canViewField($module, $field, $entity));
|
|
|
+Blade::if('fieldUpdate', fn($module, $field, $entity = null) => canUpdateField($module, $field, $entity));
|
|
|
+```
|
|
|
+
|
|
|
+Blade используется только для отображения. Серверная проверка всё равно обязательна.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 3: Helper функции
|
|
|
+## Видимость элементов интерфейса
|
|
|
+
|
|
|
+Все элементы интерфейса должны отображаться или скрываться на основе effective permissions текущего пользователя.
|
|
|
+
|
|
|
+Проверять нужно не только страницы целиком, но и отдельные элементы:
|
|
|
+
|
|
|
+- пункты основного меню;
|
|
|
+- ссылки в меню пользователя и административном меню;
|
|
|
+- кнопки создания, редактирования, удаления, восстановления;
|
|
|
+- кнопки импорта, экспорта, генерации документов;
|
|
|
+- кнопки загрузки/удаления файлов, фото, сертификатов, паспортов;
|
|
|
+- вкладки, табы, панели и блоки страницы;
|
|
|
+- модальные окна и кнопки, которые их открывают;
|
|
|
+- массовые действия;
|
|
|
+- inline-действия в строках таблиц;
|
|
|
+- dropdown/actions меню;
|
|
|
+- ajax-кнопки смены статуса;
|
|
|
+- ссылки на карточки сущностей, если у пользователя нет права просмотра;
|
|
|
+- поля таблиц и формы.
|
|
|
+
|
|
|
+Правила:
|
|
|
+
|
|
|
+- меню модуля показывается при наличии `{module}.view` или другого права, которое открывает полезный сценарий внутри модуля;
|
|
|
+- кнопка действия показывается только при наличии соответствующего action permission;
|
|
|
+- поле формы показывается при наличии `{module}.fields.{field}.view`;
|
|
|
+- поле формы доступно для редактирования только при наличии `{module}.fields.{field}.update` и action permission на редактирование сущности;
|
|
|
+- если действие недоступно, предпочтительно скрывать элемент, а не показывать disabled, кроме случаев, где disabled нужен для объяснения состояния;
|
|
|
+- для `admin` все элементы доступны, кроме ограничений бизнес-логики;
|
|
|
+- скрытие элемента интерфейса не заменяет middleware, FormRequest, Policy или проверку в сервисе.
|
|
|
|
|
|
-### Обновление `app/Helpers/roles.php`
|
|
|
+Для повторяемых элементов желательно использовать общие Blade-компоненты/partials, чтобы проверки не расходились:
|
|
|
|
|
|
-- `getRoles()` - сначала из БД, fallback на константы
|
|
|
-- `hasRole()` - использовать метод модели User
|
|
|
-- `hasPermission()` - новая функция
|
|
|
-- `hasAnyPermission()` - новая функция
|
|
|
-- `roleName()` - сначала из БД, fallback на константы
|
|
|
+```blade
|
|
|
+@permission('catalog.create')
|
|
|
+ <a href="{{ route('catalog.create') }}" class="btn btn-sm btn-primary">Добавить</a>
|
|
|
+@endpermission
|
|
|
+
|
|
|
+@fieldView('catalog', 'product_price')
|
|
|
+ ...
|
|
|
+@endfieldView
|
|
|
+```
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 4: Middleware
|
|
|
+## Middleware
|
|
|
+
|
|
|
+### Обновить `EnsureUserHasRole`
|
|
|
|
|
|
-### 4.1 Обновить `app/Http/Middleware/EnsureUserHasRole.php`
|
|
|
-- Использовать метод `$user->hasRole()` вместо прямой проверки
|
|
|
+Использовать `$request->user()->hasRole($roles)`.
|
|
|
|
|
|
-### 4.2 Новый `app/Http/Middleware/EnsureUserHasPermission.php`
|
|
|
-- Использование: `middleware('permission:orders.create')`
|
|
|
+### Новый `EnsureUserHasPermission`
|
|
|
|
|
|
-### 4.3 Новый `app/Http/Middleware/EnsureUserHasAnyPermission.php`
|
|
|
-- Использование: `middleware('permission.any:orders.create,orders.update')`
|
|
|
+Пример:
|
|
|
+
|
|
|
+```php
|
|
|
+Route::post('catalog/{product}', [ProductController::class, 'update'])
|
|
|
+ ->middleware('permission:catalog.update');
|
|
|
+```
|
|
|
+
|
|
|
+### Новый `EnsureUserHasAnyPermission`
|
|
|
+
|
|
|
+Пример:
|
|
|
+
|
|
|
+```php
|
|
|
+Route::middleware('permission.any:orders.view,reclamations.view')->group(...);
|
|
|
+```
|
|
|
+
|
|
|
+### Регистрация в `bootstrap/app.php`
|
|
|
|
|
|
-### 4.4 Регистрация в `bootstrap/app.php`
|
|
|
```php
|
|
|
$middleware->alias([
|
|
|
'role' => EnsureUserHasRole::class,
|
|
|
@@ -109,181 +472,584 @@ $middleware->alias([
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 5: Blade директивы
|
|
|
+## UI настройки доступа
|
|
|
+
|
|
|
+### Общие требования
|
|
|
+
|
|
|
+UI должен быть удобным при большом числе прав:
|
|
|
+
|
|
|
+- группировка по модулям;
|
|
|
+- внутри модуля отдельные секции: "Действия", "Поля: просмотр", "Поля: редактирование", "Файлы/документы", "Импорт/экспорт";
|
|
|
+- поиск по названию права, slug, полю;
|
|
|
+- быстрые действия: "разрешить всё в модуле", "запретить всё в модуле", "только просмотр", "сбросить";
|
|
|
+- три состояния для каждого права: не задано, allow, deny;
|
|
|
+- визуально показывать источник права: роль или пользовательское переопределение;
|
|
|
+- предупреждать о конфликте allow/deny;
|
|
|
+- показывать итоговый effective access для выбранной роли/пользователя.
|
|
|
+- действие "Скопировать роль" для создания похожей роли без наследования.
|
|
|
+- действие "Создать роль из пользователя" на основе итоговой совокупности разрешений пользователя.
|
|
|
+- удаление роли доступно только если к роли не привязан ни один пользователь.
|
|
|
+- разрешения можно менять у всех ролей, кроме `admin`.
|
|
|
+
|
|
|
+### Структура группировки
|
|
|
+
|
|
|
+Пример для каталога:
|
|
|
+
|
|
|
+```text
|
|
|
+Каталог
|
|
|
+ Действия
|
|
|
+ Просмотр каталога
|
|
|
+ Создание товара
|
|
|
+ Редактирование товара
|
|
|
+ Удаление товара
|
|
|
+ Импорт
|
|
|
+ Экспорт
|
|
|
+ Загрузка сертификата
|
|
|
+ Удаление сертификата
|
|
|
+ Поля: просмотр
|
|
|
+ Артикул
|
|
|
+ Наименование по ТЗ
|
|
|
+ Цена товара
|
|
|
+ Цена установки
|
|
|
+ Итоговая цена
|
|
|
+ Поля: редактирование
|
|
|
+ Артикул
|
|
|
+ Наименование по ТЗ
|
|
|
+ Цена товара
|
|
|
+ Цена установки
|
|
|
+ Итоговая цена
|
|
|
+```
|
|
|
+
|
|
|
+### Управление ролями
|
|
|
+
|
|
|
+Контроллер:
|
|
|
+
|
|
|
+```text
|
|
|
+app/Http/Controllers/Admin/RoleController.php
|
|
|
+```
|
|
|
|
|
|
-### Добавить в `app/Providers/AppServiceProvider.php`
|
|
|
+Маршруты:
|
|
|
|
|
|
```php
|
|
|
-Blade::if('role', fn($roles) => hasRole($roles));
|
|
|
-Blade::if('permission', fn($permission) => hasPermission($permission));
|
|
|
-Blade::if('anypermission', fn($permissions) => hasAnyPermission($permissions));
|
|
|
+Route::prefix('admin/roles')
|
|
|
+ ->name('admin.roles.')
|
|
|
+ ->middleware('permission:admin.roles')
|
|
|
+ ->group(function () {
|
|
|
+ Route::get('', [RoleController::class, 'index'])->name('index');
|
|
|
+ Route::get('create', [RoleController::class, 'create'])->name('create');
|
|
|
+ Route::post('', [RoleController::class, 'store'])->name('store');
|
|
|
+ Route::get('{role}', [RoleController::class, 'edit'])->name('edit');
|
|
|
+ Route::put('{role}', [RoleController::class, 'update'])->name('update');
|
|
|
+ Route::delete('{role}', [RoleController::class, 'destroy'])->name('destroy');
|
|
|
+ Route::post('{role}/permissions', [RoleController::class, 'syncPermissions'])->name('permissions.sync');
|
|
|
+ Route::post('{role}/copy', [RoleController::class, 'copy'])->name('copy');
|
|
|
+ Route::post('from-user/{user}', [RoleController::class, 'storeFromUser'])->name('store-from-user');
|
|
|
+ });
|
|
|
```
|
|
|
|
|
|
-**Использование:**
|
|
|
-```blade
|
|
|
-@role('admin')...@endrole
|
|
|
-@permission('orders.delete')...@endpermission
|
|
|
+Views:
|
|
|
+
|
|
|
+```text
|
|
|
+resources/views/admin/roles/index.blade.php
|
|
|
+resources/views/admin/roles/edit.blade.php
|
|
|
+resources/views/admin/roles/partials/permission-matrix.blade.php
|
|
|
```
|
|
|
|
|
|
----
|
|
|
+Правила управления ролями:
|
|
|
+
|
|
|
+- `admin` нельзя удалить, деактивировать, переименовать по slug или изменить по permissions;
|
|
|
+- для `admin` не показывать чекбоксы снятия permissions и deny-состояния, либо показывать их disabled;
|
|
|
+- для `admin` нельзя сохранять deny в `role_permissions`;
|
|
|
+- роль можно удалить только если `users.role_id` не содержит ссылок на неё;
|
|
|
+- перед удалением роли проверять пользователей с учётом soft-deleted записей;
|
|
|
+- если роль используется, UI должен показать список или количество пользователей, из-за которых удаление недоступно;
|
|
|
+- системные роли можно защищать от удаления slug, но permissions всех системных ролей, кроме `admin`, можно менять.
|
|
|
+
|
|
|
+Копирование роли:
|
|
|
+
|
|
|
+- создаёт новую роль с новым `slug` и `name`;
|
|
|
+- копирует все `role_permissions` исходной роли;
|
|
|
+- новая роль не связана с исходной;
|
|
|
+- дальнейшее изменение исходной роли не влияет на копию.
|
|
|
+
|
|
|
+Создание роли из пользователя:
|
|
|
|
|
|
-## Фаза 6: Сидер данных
|
|
|
+- берёт `getEffectivePermissions()` выбранного пользователя после применения роли и индивидуальных overrides;
|
|
|
+- создаёт новую роль с этими permissions как собственными `role_permissions`;
|
|
|
+- `allow` записывается для всех итогово разрешённых permissions;
|
|
|
+- итогово запрещённые permissions не записываются, кроме случаев, где для новой роли нужен явный deny;
|
|
|
+- индивидуальные `user_permissions` исходного пользователя в новую роль не переносятся как связь, а материализуются в матрицу роли;
|
|
|
+- после создания роли можно назначить её другим пользователям без копирования индивидуальных overrides.
|
|
|
|
|
|
-### `database/seeders/RbacSeeder.php`
|
|
|
+### Управление индивидуальными правами пользователя
|
|
|
|
|
|
-1. Создать 73 права (permissions) по модулям
|
|
|
-2. Создать системные роли (admin, manager, brigadier) с `is_system=true`
|
|
|
-3. Назначить права ролям
|
|
|
-4. Мигрировать существующих пользователей: заполнить `role_id` на основе `role`
|
|
|
+В форме пользователя добавить вкладки:
|
|
|
+
|
|
|
+- "Профиль";
|
|
|
+- "Роль";
|
|
|
+- "Индивидуальные права";
|
|
|
+- "Итоговый доступ".
|
|
|
+
|
|
|
+Важно: пользовательские права не заменяют роль, а накладываются поверх неё.
|
|
|
+
|
|
|
+Для пользователя с ролью `admin` запрещено создавать индивидуальные deny. Если пользователь является администратором, его итоговый доступ всегда all allow.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Фаза 7: UI управления ролями
|
|
|
+## Карта модулей и полей
|
|
|
+
|
|
|
+Нужен единый источник прав и полей, чтобы не собирать permissions вручную в разных местах.
|
|
|
|
|
|
-### 7.1 Контроллер `app/Http/Controllers/Admin/RoleController.php`
|
|
|
-- CRUD для ролей
|
|
|
-- Назначение прав через чекбоксы
|
|
|
+### Вариант: config/access.php
|
|
|
|
|
|
-### 7.2 Маршруты (добавить в `routes/web.php`)
|
|
|
```php
|
|
|
-Route::prefix('admin/roles')->middleware('role:admin')->group(function(){
|
|
|
- Route::get('', [RoleController::class, 'index'])->name('admin.roles.index');
|
|
|
- Route::get('create', [RoleController::class, 'create'])->name('admin.roles.create');
|
|
|
- Route::post('', [RoleController::class, 'store'])->name('admin.roles.store');
|
|
|
- Route::get('{role}', [RoleController::class, 'show'])->name('admin.roles.show');
|
|
|
- Route::put('{role}', [RoleController::class, 'update'])->name('admin.roles.update');
|
|
|
- Route::delete('{role}', [RoleController::class, 'destroy'])->name('admin.roles.destroy');
|
|
|
-});
|
|
|
-```
|
|
|
-
|
|
|
-### 7.3 Views
|
|
|
-- `resources/views/admin/roles/index.blade.php` - список ролей
|
|
|
-- `resources/views/admin/roles/edit.blade.php` - создание/редактирование с чекбоксами прав
|
|
|
-
|
|
|
-### 7.4 Меню
|
|
|
-Добавить в `resources/views/layouts/menu.blade.php`:
|
|
|
-```blade
|
|
|
-<li><a href="{{ route('admin.roles.index') }}">Роли и права</a></li>
|
|
|
+return [
|
|
|
+ 'catalog' => [
|
|
|
+ 'name' => 'Каталог',
|
|
|
+ 'entity' => 'product',
|
|
|
+ 'actions' => [
|
|
|
+ 'view' => 'Просмотр',
|
|
|
+ 'create' => 'Создание',
|
|
|
+ 'update' => 'Редактирование',
|
|
|
+ 'delete' => 'Удаление',
|
|
|
+ 'import' => 'Импорт',
|
|
|
+ 'export' => 'Экспорт',
|
|
|
+ 'certificates.upload' => 'Загрузка сертификата',
|
|
|
+ 'certificates.delete' => 'Удаление сертификата',
|
|
|
+ ],
|
|
|
+ 'fields' => [
|
|
|
+ 'article' => 'Артикул',
|
|
|
+ 'nomenclature_number' => 'Номер номенклатуры',
|
|
|
+ 'name_tz' => 'Наименование по ТЗ',
|
|
|
+ 'type_tz' => 'Тип по ТЗ',
|
|
|
+ 'manufacturer' => 'Производитель',
|
|
|
+ 'unit' => 'Ед. изм.',
|
|
|
+ 'type' => 'Тип',
|
|
|
+ 'manufacturer_name' => 'Наименование производителя',
|
|
|
+ 'sizes' => 'Размеры',
|
|
|
+ 'product_price' => 'Цена товара',
|
|
|
+ 'installation_price' => 'Цена установки',
|
|
|
+ 'total_price' => 'Итоговая цена',
|
|
|
+ 'note' => 'Примечания',
|
|
|
+ 'certificate_id' => 'Сертификат',
|
|
|
+ 'passport_name' => 'Наименование по паспорту',
|
|
|
+ 'statement_name' => 'Наименование в ведомости',
|
|
|
+ 'service_life' => 'Срок службы',
|
|
|
+ 'certificate_number' => 'Номер сертификата',
|
|
|
+ 'certificate_date' => 'Дата сертификата',
|
|
|
+ 'certificate_issuer' => 'Орган сертификации',
|
|
|
+ 'certificate_type' => 'Вид сертификации',
|
|
|
+ 'weight' => 'Вес',
|
|
|
+ 'volume' => 'Объем',
|
|
|
+ 'places' => 'Мест',
|
|
|
+ ],
|
|
|
+ ],
|
|
|
+];
|
|
|
```
|
|
|
|
|
|
+Сидер должен создавать permissions из этого конфига.
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## Фаза 8: Обновление формы пользователя
|
|
|
+## Применение field-level ACL в коде
|
|
|
+
|
|
|
+### Меню и действия
|
|
|
+
|
|
|
+Переделать существующие Blade-проверки `hasRole()` для меню и action-кнопок на permissions:
|
|
|
+
|
|
|
+```blade
|
|
|
+@permission('catalog.view')
|
|
|
+ <a href="{{ route('catalog.index') }}">Каталог</a>
|
|
|
+@endpermission
|
|
|
+
|
|
|
+@permission('catalog.export')
|
|
|
+ <button type="button" data-bs-toggle="modal" data-bs-target="#exportModal">Экспорт</button>
|
|
|
+@endpermission
|
|
|
+```
|
|
|
+
|
|
|
+Особое внимание:
|
|
|
+
|
|
|
+- `resources/views/layouts/menu.blade.php`;
|
|
|
+- action toolbar в index/edit views;
|
|
|
+- `resources/views/partials/table.blade.php` для inline-действий;
|
|
|
+- модалки импорта/экспорта;
|
|
|
+- кнопки загрузки и удаления файлов.
|
|
|
+
|
|
|
+### Таблицы
|
|
|
+
|
|
|
+Сейчас общий partial таблиц выводит все `$header`. Нужно фильтровать `$header` до передачи во view или внутри helper:
|
|
|
+
|
|
|
+```php
|
|
|
+$this->data['header'] = app(FieldAccessService::class)
|
|
|
+ ->visibleHeaders($request->user(), 'catalog', $this->data['header']);
|
|
|
+```
|
|
|
+
|
|
|
+Для полей с accessor `*_txt` нужна связь с реальным полем:
|
|
|
+
|
|
|
+```text
|
|
|
+product_price_txt -> product_price
|
|
|
+installation_price_txt -> installation_price
|
|
|
+total_price_txt -> total_price
|
|
|
+```
|
|
|
+
|
|
|
+### Формы
|
|
|
+
|
|
|
+Для каждого input:
|
|
|
+
|
|
|
+- если нет `field.view` - не показывать значение;
|
|
|
+- если есть `field.view`, но нет `field.update` - показывать readonly/disabled;
|
|
|
+- если нет `catalog.update` - вся форма readonly;
|
|
|
+- hidden required-поля не валидируются как required для пользователя без `field.view`;
|
|
|
+- readonly/disabled-поля не приходят в request или могут быть подделаны, поэтому backend не должен полагаться на disabled;
|
|
|
+- если create требует поле, которое пользователь не может заполнить, create нужно запретить целиком, кроме случаев с безопасным default.
|
|
|
+
|
|
|
+### FormRequest и сохранение
|
|
|
+
|
|
|
+Нельзя просто делать:
|
|
|
+
|
|
|
+```php
|
|
|
+$product->update($request->validated());
|
|
|
+```
|
|
|
+
|
|
|
+Нужно:
|
|
|
+
|
|
|
+```php
|
|
|
+$data = app(AccessService::class)
|
|
|
+ ->filterWritableData($request->user(), 'catalog', $request->validated(), 'product');
|
|
|
+
|
|
|
+$product->update($data);
|
|
|
+```
|
|
|
|
|
|
-### Обновить `app/Http/Requests/User/StoreUser.php`
|
|
|
-- Заменить `role` на `role_id`
|
|
|
+Для `create` можно использовать `catalog.fields.*.create` или считать, что `create` использует `update`-права полей. Лучше явно добавить field action `create`, если создание должно отличаться от редактирования.
|
|
|
|
|
|
-### Обновить view редактирования пользователя
|
|
|
-- Выпадающий список ролей из БД вместо констант
|
|
|
+### Фильтры, сортировка, поиск
|
|
|
+
|
|
|
+Если поле скрыто, пользователь не должен:
|
|
|
+
|
|
|
+- видеть фильтр по нему;
|
|
|
+- сортировать по нему;
|
|
|
+- искать по нему, если поиск раскрывает наличие значения;
|
|
|
+- получать side-channel через количество результатов.
|
|
|
+
|
|
|
+Для каталога это особенно важно для цен.
|
|
|
+
|
|
|
+### Экспорт
|
|
|
+
|
|
|
+Экспорт регулируется action permission, например `catalog.export`. Если пользователь имеет право на экспорт, экспорт может содержать полный набор полей для этой операции. Назначение export permission считается ответственностью администратора ролей.
|
|
|
+
|
|
|
+### Импорт
|
|
|
+
|
|
|
+Импорт регулируется action permission, например `catalog.import`. Если пользователь имеет право на импорт, импорт может принимать полный набор колонок для этой операции. Назначение import permission считается ответственностью администратора ролей.
|
|
|
+
|
|
|
+### Ajax и search endpoints
|
|
|
+
|
|
|
+Проверить маршруты:
|
|
|
+
|
|
|
+- `product/search`;
|
|
|
+- `order-search`;
|
|
|
+- `pricing-codes/get-description`;
|
|
|
+- `pricing-codes/search`;
|
|
|
+- любые endpoint'ы статусов, файлов, чатов.
|
|
|
+
|
|
|
+Они должны проверять action permission и не отдавать скрытые поля.
|
|
|
+
|
|
|
+### Документы и генерация файлов
|
|
|
+
|
|
|
+Документы и сгенерированные файлы выдаются целиком. Если пользователь имеет action permission на документ, например `orders.documents.generate`, он получает полный документ без маскирования отдельных полей.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Список прав (73 права)
|
|
|
+## Модули и стартовый набор прав
|
|
|
+
|
|
|
+Минимальный набор action permissions:
|
|
|
|
|
|
-| Модуль | Права |
|
|
|
-|--------|-------|
|
|
|
-| orders | view, create, update, delete, export, documents, photos, maf, generate |
|
|
|
-| reclamations | view, create, update, delete, documents, photos, spare_parts |
|
|
|
-| maf | view, update, import, export, passports |
|
|
|
-| maf_orders | view, create, update, delete |
|
|
|
-| catalog | view, create, update, delete, import, export, certificates |
|
|
|
+| Модуль | Action permissions |
|
|
|
+|--------|--------------------|
|
|
|
+| orders | view, create, update, delete, export, documents.generate, documents.upload, documents.delete, photos.upload, photos.delete, maf.manage, ttn.create |
|
|
|
+| reclamations | view, create, update, delete, export, documents.generate, documents.upload, documents.delete, photos.upload, photos.delete, act.upload, act.delete, spare_parts.manage |
|
|
|
+| maf | view, update, import, export, passports.upload, passports.delete |
|
|
|
+| maf_orders | view, create, update, delete, stock.manage |
|
|
|
+| catalog | view, create, update, delete, import, export, certificates.upload, certificates.delete, thumbnail.upload |
|
|
|
| schedule | view, create, update, delete, export |
|
|
|
-| spare_parts | view, create, update, delete, import, export |
|
|
|
-| spare_part_orders | view, create, update, delete, ship |
|
|
|
-| spare_part_reservations | view, manage |
|
|
|
+| spare_parts | view, create, update, delete, import, export, image.upload |
|
|
|
+| spare_part_orders | view, create, update, delete, ship, stock.manage, correct |
|
|
|
+| spare_part_reservations | view, manage, issue, cancel, reassign |
|
|
|
| spare_part_inventory | view |
|
|
|
| contracts | view, create, update, delete |
|
|
|
-| users | view, create, update, delete |
|
|
|
+| contractors | view, create, update, delete, prices.update, prices.import, prices.export |
|
|
|
+| users | view, create, update, delete, restore, impersonate, permissions.manage |
|
|
|
| responsibles | view, create, update, delete |
|
|
|
| reports | view |
|
|
|
| import | view, create |
|
|
|
-| districts | view, manage |
|
|
|
-| areas | view, manage |
|
|
|
-| pricing_codes | view, manage |
|
|
|
-| admin | clear_data, year_data, roles |
|
|
|
+| districts | view, create, update, delete, restore, import, export |
|
|
|
+| areas | view, create, update, delete, restore, import, export |
|
|
|
+| pricing_codes | view, create, update, delete, search |
|
|
|
+| notifications | view, mark_read |
|
|
|
+| admin | settings, clear_data, year_data, roles |
|
|
|
+
|
|
|
+Для каждого модуля с табличными данными дополнительно генерируются field permissions:
|
|
|
+
|
|
|
+```text
|
|
|
+{module}.fields.{field}.view
|
|
|
+{module}.fields.{field}.update
|
|
|
+```
|
|
|
+
|
|
|
+Экспорт, импорт и документы не имеют field-level masking и регулируются action permissions.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Права по ролям (по умолчанию)
|
|
|
+## Дефолтные роли
|
|
|
+
|
|
|
+После применения RBAC все базовые роли должны получить такие же фактические права, как в текущем коде до миграции. Это обязательное требование миграции: пользователь с той же базовой ролью не должен внезапно получить меньше или больше доступа.
|
|
|
+
|
|
|
+Стартовая матрица прав формируется не "на глаз", а по аудиту текущих источников доступа:
|
|
|
+
|
|
|
+- `routes/web.php` middleware `role:*`;
|
|
|
+- `hasRole()` в Blade;
|
|
|
+- `hasRole()` в контроллерах и FormRequest;
|
|
|
+- `Role::effectiveRoles()`;
|
|
|
+- прямые выборки `User::where('role', ...)` / `whereIn('role', ...)`;
|
|
|
+- object-level ограничения в контроллерах.
|
|
|
+
|
|
|
+Для спорных мест права фиксируются по текущему фактическому поведению, а изменение бизнес-логики выносится в отдельную задачу после внедрения RBAC.
|
|
|
|
|
|
### Admin
|
|
|
-Все права (*)
|
|
|
+
|
|
|
+- все action permissions allow;
|
|
|
+- все field permissions allow;
|
|
|
+- нельзя удалить/деактивировать последнего admin;
|
|
|
+- нельзя изменить permissions роли `admin`;
|
|
|
+- нельзя поставить deny роли `admin`;
|
|
|
+- нельзя снять разрешения с роли `admin`;
|
|
|
+- нельзя поставить индивидуальный deny пользователю с ролью `admin`;
|
|
|
+- нельзя снять с самого себя роль `admin`, если это последний активный администратор.
|
|
|
|
|
|
### Manager
|
|
|
-- orders: view, create, update, documents, photos, generate
|
|
|
-- reclamations: view, create, update, documents, photos, spare_parts
|
|
|
-- maf: view, update
|
|
|
-- catalog: view
|
|
|
-- schedule: view, create, update
|
|
|
-- spare_parts: view
|
|
|
-- spare_part_orders: view, create, update
|
|
|
-- spare_part_reservations: view, manage
|
|
|
-- spare_part_inventory: view
|
|
|
-- contracts: view, create, update
|
|
|
-- responsibles: view, create, update
|
|
|
-- reports: view
|
|
|
+
|
|
|
+База должна соответствовать текущему поведению `role:admin,manager`:
|
|
|
+
|
|
|
+- orders: view, update, documents/photos where currently allowed, export where route allows manager;
|
|
|
+- reclamations: view, create, update, export, documents/photos/act according to current routes;
|
|
|
+- catalog: view;
|
|
|
+- contracts: view, create, update, delete currently route group allows manager, но нужно проверить бизнес-ожидание;
|
|
|
+- responsibles: view, create, update, delete;
|
|
|
+- reports: view;
|
|
|
+- spare_parts: view;
|
|
|
+- spare_part_orders: view, create, update, ship, stock.manage;
|
|
|
+- spare_part_reservations: view, manage;
|
|
|
+- spare_part_inventory: view;
|
|
|
+- schedule: view.
|
|
|
+
|
|
|
+Поля цен каталога для manager нужно задать явно:
|
|
|
+
|
|
|
+- если менеджер должен видеть цены: allow `catalog.fields.*price*.view`;
|
|
|
+- если нет: deny `catalog.fields.product_price.view`, `installation_price.view`, `total_price.view` и соответствующие `*_txt`.
|
|
|
|
|
|
### Brigadier
|
|
|
-- orders: view, photos
|
|
|
-- reclamations: view, photos
|
|
|
-- schedule: view
|
|
|
+
|
|
|
+База по текущему коду:
|
|
|
+
|
|
|
+- orders: view, photos.upload, photos.view;
|
|
|
+- reclamations: view, photos.upload, act.upload;
|
|
|
+- schedule: view только своих записей, если применяется объектное ограничение.
|
|
|
+
|
|
|
+Важно: часть текущих ограничений не role-based, а object-level: бригадир видит свои заказы/рекламации. Это не заменяется RBAC и должно остаться отдельной проверкой.
|
|
|
+
|
|
|
+### Warehouse Head
|
|
|
+
|
|
|
+Сейчас используется минимум в рекламациях для upload act и в уведомлениях. Нужно отдельно согласовать набор прав:
|
|
|
+
|
|
|
+- reclamations.act.upload;
|
|
|
+- spare_parts/spare_part_orders/spare_part_inventory по фактической бизнес-роли склада.
|
|
|
+
|
|
|
+### Assistant Head
|
|
|
+
|
|
|
+Текущее `effectiveRoles()` даёт `assistant_head` права `admin` и `manager`.
|
|
|
+
|
|
|
+В целевой модели наследования нет. Поэтому миграция должна создать для `assistant_head` собственную полную матрицу permissions, которая повторяет текущий фактический доступ этой роли. После этого `assistant_head` редактируется как самостоятельная роль.
|
|
|
+
|
|
|
+Если текущий доступ `assistant_head` через admin считается слишком широким, это нужно менять отдельным бизнес-решением после фиксации базовой миграции, а не в рамках технического переноса RBAC.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Порядок выполнения
|
|
|
-
|
|
|
-1. Создать миграции (4 файла)
|
|
|
-2. Создать модель Permission
|
|
|
-3. Обновить модель Role (сохранить константы)
|
|
|
-4. Обновить модель User
|
|
|
-5. Обновить helpers/roles.php
|
|
|
-6. Обновить EnsureUserHasRole middleware
|
|
|
-7. Создать новые middleware (permission, permission.any)
|
|
|
-8. Зарегистрировать middleware в bootstrap/app.php
|
|
|
-9. Добавить Blade директивы в AppServiceProvider
|
|
|
-10. Создать RbacSeeder
|
|
|
-11. Запустить: `php artisan migrate && php artisan db:seed --class=RbacSeeder`
|
|
|
-12. Создать RoleController
|
|
|
-13. Создать views для ролей
|
|
|
-14. Обновить меню
|
|
|
-15. Обновить форму пользователя (role → role_id)
|
|
|
+## Object-level ограничения
|
|
|
+
|
|
|
+RBAC/field ACL не заменяет ограничения по конкретной записи:
|
|
|
+
|
|
|
+- бригадир видит только свои заказы/рекламации;
|
|
|
+- пользователь не должен редактировать чужой профиль;
|
|
|
+- нельзя удалить самого себя;
|
|
|
+- нельзя удалить последнего admin;
|
|
|
+- soft-deleted пользователи и impersonation имеют отдельные ограничения;
|
|
|
+- доступ к файлам должен проверять принадлежность файла сущности и право на действие.
|
|
|
+
|
|
|
+Для этого нужны Policies:
|
|
|
+
|
|
|
+```text
|
|
|
+OrderPolicy
|
|
|
+ReclamationPolicy
|
|
|
+ProductPolicy
|
|
|
+SchedulePolicy
|
|
|
+UserPolicy
|
|
|
+```
|
|
|
+
|
|
|
+Policies должны использовать `AccessService`, но также проверять ownership/status/state.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Обратная совместимость
|
|
|
+## Миграция текущего кода
|
|
|
+
|
|
|
+### Фаза 1. Инфраструктура
|
|
|
+
|
|
|
+1. Создать migrations: `roles`, `permissions`, `role_permissions`, `user_permissions`, `users.role_id`.
|
|
|
+2. Создать `Permission` model.
|
|
|
+3. Преобразовать `Role` в Eloquent model с сохранением констант и fallback.
|
|
|
+4. Добавить методы в `User`.
|
|
|
+5. Создать `AccessService` и `FieldAccessService`.
|
|
|
+6. Обновить helpers.
|
|
|
+7. Добавить middleware `permission` и `permission.any`.
|
|
|
+8. Добавить Blade directives.
|
|
|
+9. Добавить `config/access.php`.
|
|
|
+10. Создать сидер permissions из `config/access.php`.
|
|
|
+
|
|
|
+### Фаза 2. Сидер и обратная совместимость
|
|
|
+
|
|
|
+1. Создать системные роли: все 5 текущих ролей.
|
|
|
+2. Заполнить `role_id` у пользователей по `users.role`.
|
|
|
+3. Создать стартовую матрицу прав по `docs/flex_roles_access_inventory.md`, полностью повторяющую текущий фактический доступ базовых ролей.
|
|
|
+4. Для `assistant_head` развернуть текущее `effectiveRoles()` в собственный набор permissions без наследования.
|
|
|
+5. Сохранить `users.role`, `Role::NAMES`, `Role::VALID_ROLES`, `hasRole()`.
|
|
|
+6. Добавить тесты, что базовые роли после миграции имеют тот же доступ, что до миграции.
|
|
|
+
|
|
|
+### Фаза 3. UI ролей и пользователей
|
|
|
+
|
|
|
+1. CRUD ролей.
|
|
|
+2. Permission matrix с группировкой.
|
|
|
+3. Индивидуальные права пользователя.
|
|
|
+4. Экран effective access.
|
|
|
+5. Кнопка "Скопировать роль" для создания независимой роли на основе существующей.
|
|
|
+6. Кнопка "Создать роль из пользователя" на основе effective permissions выбранного пользователя.
|
|
|
+7. Удаление роли только при отсутствии привязанных пользователей, включая soft-deleted.
|
|
|
+8. Защита системных ролей от удаления slug и критичных изменений.
|
|
|
+9. Жёсткая защита permissions роли `admin`: нельзя снять allow, поставить deny или сохранить ограничивающие user overrides для admin-пользователей.
|
|
|
+
|
|
|
+### Фаза 4. Перевод routes/FormRequest
|
|
|
+
|
|
|
+1. Постепенно заменить `role:*` на `permission:*`.
|
|
|
+2. FormRequest `authorize()` перевести на permissions.
|
|
|
+3. Контроллеры с ручным `hasRole()` перевести на `AccessService`.
|
|
|
+4. Прямые `where('role')` заменить на scope/helpers, работающие с `role_id` и fallback.
|
|
|
+
|
|
|
+### Фаза 5. Field-level ACL
|
|
|
+
|
|
|
+1. Начать с `catalog`, потому что там есть явный кейс цен.
|
|
|
+2. Фильтровать headers таблиц.
|
|
|
+3. Фильтровать поля формы.
|
|
|
+4. Фильтровать update payload.
|
|
|
+5. Фильтровать фильтры/сортировки/поиск.
|
|
|
+6. Проверить action permissions для export/import; field masking для export/import не применяется.
|
|
|
+7. Покрыть тестами.
|
|
|
+8. Расширить на orders, reclamations, spare_parts, users и остальные модули.
|
|
|
+
|
|
|
+### Фаза 6. Очистка legacy
|
|
|
+
|
|
|
+После полного перевода:
|
|
|
+
|
|
|
+1. убрать прямое использование `users.role` из запросов;
|
|
|
+2. оставить `users.role` как computed/fallback или удалить отдельной миграцией;
|
|
|
+3. заменить `Role::NAMES` на БД/кэш;
|
|
|
+4. удалить устаревшие проверки `hasRole()` там, где они больше не нужны.
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-- Функция `hasRole()` продолжает работать
|
|
|
-- Middleware `role:admin,manager` работает без изменений
|
|
|
-- Blade шаблоны с `@if(hasRole('admin'))` работают
|
|
|
-- Постепенная миграция на `hasPermission()` опциональна
|
|
|
+## Особые риски
|
|
|
+
|
|
|
+1. **Скрытие поля только в Blade не защищает данные.** Нужно фильтровать backend payload.
|
|
|
+2. **Фильтры и сортировка по скрытым полям раскрывают данные косвенно.** Для цен это критично.
|
|
|
+3. **Импорт может обновить скрытые поля.** Это принято как ответственность администратора ролей: import permission даёт полный импорт.
|
|
|
+4. **Экспорт может раскрыть скрытые поля.** Это принято как ответственность администратора ролей: export permission даёт полный экспорт.
|
|
|
+5. **`assistant_head` сейчас получает права через `effectiveRoles()`.** При переносе нужно развернуть это в явную матрицу permissions, иначе можно случайно расширить или урезать доступ.
|
|
|
+6. **Прямые запросы по `users.role` сломают новые роли.** Их нужно заменить.
|
|
|
+7. **Кэш permissions может устареть.** Нужна централизованная инвалидация.
|
|
|
+8. **Индивидуальные deny могут заблокировать пользователя от UI ролей.** Нужна защита от самоблокировки.
|
|
|
+9. **Object-level правила не выражаются простым RBAC.** Нужны Policies/scopes.
|
|
|
+10. **Документы могут содержать скрытые поля.** Это принято как all-or-nothing: document action permission отдаёт полный документ.
|
|
|
+11. **Удаление роли может оставить пользователей без доступа.** Удалять роль можно только после проверки отсутствия связанных пользователей.
|
|
|
+12. **Ограничение admin ломает аварийный доступ.** Для роли `admin` запрещены deny и снятие permissions.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Критические файлы для изменения
|
|
|
-
|
|
|
-| Файл | Действие |
|
|
|
-|------|----------|
|
|
|
-| `app/Models/Role.php` | Переработать в Eloquent модель |
|
|
|
-| `app/Models/User.php` | Добавить role_id, методы hasPermission |
|
|
|
-| `app/Models/Permission.php` | Создать новую модель |
|
|
|
-| `app/Helpers/roles.php` | Обновить с поддержкой БД |
|
|
|
-| `app/Http/Middleware/EnsureUserHasRole.php` | Использовать метод модели |
|
|
|
-| `bootstrap/app.php` | Зарегистрировать новые middleware |
|
|
|
-| `app/Providers/AppServiceProvider.php` | Добавить Blade директивы |
|
|
|
-| `database/seeders/RbacSeeder.php` | Создать сидер |
|
|
|
-| `app/Http/Controllers/Admin/RoleController.php` | Создать контроллер |
|
|
|
-| `resources/views/admin/roles/*.blade.php` | Создать views |
|
|
|
-| `resources/views/layouts/menu.blade.php` | Добавить пункт меню |
|
|
|
-| `routes/web.php` | Добавить маршруты ролей |
|
|
|
+## Минимальный MVP
|
|
|
+
|
|
|
+Чтобы получить пользу без полного переписывания:
|
|
|
+
|
|
|
+1. Ввести таблицы roles/permissions/user_permissions.
|
|
|
+2. Сохранить `users.role` fallback.
|
|
|
+3. Реализовать `AccessService`.
|
|
|
+4. Сделать UI ролей и индивидуальных прав.
|
|
|
+5. Перевести каталог:
|
|
|
+ - `catalog.view/create/update/delete/import/export`;
|
|
|
+ - field ACL `view/update` для всех полей каталога;
|
|
|
+ - особенно `product_price`, `installation_price`, `total_price` и `*_txt`.
|
|
|
+6. Защитить `ProductController::store/update`, `StoreProductRequest`, catalog table, export/import.
|
|
|
+7. После проверки паттерна переносить остальные модули.
|
|
|
|
|
|
---
|
|
|
|
|
|
-## Верификация
|
|
|
+## Тесты
|
|
|
+
|
|
|
+Нужны feature/unit тесты:
|
|
|
+
|
|
|
+- admin имеет все права;
|
|
|
+- manager видит каталог, но не видит цены при deny;
|
|
|
+- manager не может обновить `name_tz`, если нет `catalog.fields.name_tz.update`;
|
|
|
+- скрытое required-поле не валидируется как required для пользователя без `field.view`;
|
|
|
+- readonly-поле удаляется из update payload при отсутствии `field.update`;
|
|
|
+- запрещённое поле не попадает в таблицу;
|
|
|
+- запрещённое поле нельзя использовать в filters/sort/search;
|
|
|
+- export/import/documents работают по action permission и отдают полный результат операции;
|
|
|
+- user allow добавляет право поверх роли;
|
|
|
+- user deny перекрывает role allow;
|
|
|
+- `hasRole('admin,manager')` работает с `role_id` и fallback `role`;
|
|
|
+- все 5 базовых ролей после миграции сохраняют текущий фактический доступ;
|
|
|
+- `assistant_head` получает тот же фактический доступ через собственные permissions, без role inheritance;
|
|
|
+- копирование роли создаёт независимую роль с теми же permissions;
|
|
|
+- создание роли из пользователя материализует effective permissions пользователя в новую независимую роль;
|
|
|
+- роль с привязанными пользователями нельзя удалить;
|
|
|
+- роль без привязанных пользователей можно удалить;
|
|
|
+- permissions роли `admin` нельзя изменить;
|
|
|
+- deny для роли `admin` и admin-пользователя нельзя сохранить;
|
|
|
+- brigadier object-level ограничения не ломаются.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Команды
|
|
|
+
|
|
|
+Запускать внутри контейнеров:
|
|
|
+
|
|
|
+```bash
|
|
|
+docker compose exec app php artisan migrate
|
|
|
+docker compose exec app php artisan db:seed --class=RbacSeeder
|
|
|
+make test
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
|
|
|
-После внедрения:
|
|
|
-1. Проверить авторизацию существующих пользователей
|
|
|
-2. Создать новую роль через UI
|
|
|
-3. Назначить права новой роли
|
|
|
-4. Создать пользователя с новой ролью
|
|
|
-5. Проверить доступ к разделам согласно правам
|
|
|
-6. Проверить что системные роли нельзя удалить
|
|
|
+## Критерии готовности
|
|
|
+
|
|
|
+- роли и права настраиваются через UI;
|
|
|
+- права сгруппированы по модулям и полям;
|
|
|
+- есть индивидуальные права пользователя с allow/deny;
|
|
|
+- есть создание роли копированием существующей роли;
|
|
|
+- есть создание роли из итоговых разрешений пользователя;
|
|
|
+- роль удаляется только если к ней не привязан ни один пользователь;
|
|
|
+- permissions всех ролей, кроме `admin`, можно менять;
|
|
|
+- у роли `admin` нельзя убрать права или поставить deny;
|
|
|
+- пункты меню, кнопки, вкладки, модалки, inline-действия и поля UI отображаются по permissions пользователя;
|
|
|
+- field-level ACL работает для просмотра и редактирования;
|
|
|
+- каталог может быть показан без цен;
|
|
|
+- продукт можно редактировать с запретом изменения отдельных полей;
|
|
|
+- export/import/documents доступны только по соответствующим action permissions и отдают полный результат операции;
|
|
|
+- legacy role checks не ломают текущий функционал;
|
|
|
+- прямые проверки `users.role` заменены или имеют совместимый fallback;
|
|
|
+- основные сценарии покрыты тестами.
|