Проверка доступа к задачам — fn_UserTaskPermissionsGeneral¶
Источники: DB_MSSQL (fn_UserTaskPermissionsTemplate, GenerateExtParamsRightsFunctions), core (TaskPermissionsService, GridPermissionsService, SettingsUpdateHandler)
1. Назначение¶
fn_UserTaskPermissionsGeneral — главная SQL-функция проверки доступа к задачам. Inline TVF (Table-Valued Function), возвращающая все TaskID, доступные пользователю.
Вызывается при каждом запросе грида, ленты, календаря, канбана — любого синдиката, показывающего задачи. Это самый hot-path в системе прав.
2. Кодогенерация¶
Почему не статическая функция¶
Функция генерируется из шаблона fn_UserTaskPermissionsTemplate процедурой GenerateExtParamsRightsFunctions. В шаблоне есть ссылки на dbo.Settings, которые при генерации заменяются на литералы — чтобы не делать JOIN с Settings в каждой из 31 ветви UNION ALL.
Что заменяется¶
| В шаблоне | В сгенерированной функции | Пример |
|---|---|---|
settings.InheritAccessToDirectorsF |
cast(0 as bit) или cast(1 as bit) |
По умолчанию 0 → ветви руководителей всегда false |
settings.ChatSubcatID |
Число | 969 в HD |
Дополнительная подстановка¶
Если SettingsCustom.UserTaskPermissions.ExcludeGroupsTagActions = 1:
- Строка --$$NEED_GROUP_TAG_ACTIONS раскомментируется → and 1=0 → ветвь групповых тэгов отключается
Алгоритм GenerateExtParamsRightsFunctions¶
object_definition()→ телоfn_UserTaskPermissionsTemplate- Замена имени →
fn_UserTaskPermissionsGeneral - Чтение
Settings.InheritAccessToDirectorsF,Settings.ChatSubcatID - Text replace ссылок на литералы
- Обработка
$$NEED_GROUP_TAG_ACTIONS DROP FUNCTION IF EXISTS fn_UserTaskPermissionsGeneralEXEC (@UFNText)— пересоздание
Когда перегенерируется¶
| Триггер | Где | Что меняется |
|---|---|---|
Смена InheritAccessToDirectorsF |
SettingsUpdateHandler.CheckChanges() |
Общие настройки: «Руководитель наследует просмотр» |
CRUD в ExtParamsRightsInSubcat |
EpRightsProcessorController (after-insert/update/delete) |
Конфигурация гибких прав |
| Миграция шаблона | Nakat-скрипт | После ALTER fn_UserTaskPermissionsTemplate |
Код SP: DB_MSSQL/dbo.GenerateExtParamsRightsFunctions.StoredProcedure.sql
Код C#: core/TCClassLib/Settings/SettingsUpdateHandler.cs:132-135
3. Сигнатура¶
fn_UserTaskPermissionsGeneral (
@UserID int,
@SubcatID int, -- NULL = все категории
@ActiveTasksOnlyF bit, -- 1 = только открытые
@SubcatIDNotNullOnlyF bit, -- 1 = работать только при указании категории
@WithoutAssistantsOnlyF bit, -- 1 = без замещения
@WithoutSubordinatesOnlyF bit, -- 1 = без подчинённых
@WithoutSubcatAccessF bit -- 1 = без категорийного доступа
)
RETURNS TABLE (TaskID int, UserID int)
4. Архитектура функции¶
4.1 Общая структура¶
CTE app_settings → InheritAccessToDirectorsF, ChatSubcatID (в сгенерированной: литералы)
CTE subcat_access → 0/1: есть ли у @UserID доступ к @SubcatID через DescendantSubcatAccess
CASE subcat_access.access_f:
= 0 → UNION ALL по 9 источникам (каждый с вариантами SubcatID IS NULL / IS NOT NULL)
= 1 → все задачи категории без проверки (SELECT TaskID FROM Tasks WHERE SubcatID = @SubcatID)
4.2 CTE subcat_access — оптимизация¶
subcat_access as (
select coalesce(access.access_f, 0) as access_f
from (select UserID = @UserID, SubcatID = @SubcatID) access_param
outer apply (
select 1 as access_f
from DescendantSubcatAccess dsa with (nolock)
where @WithoutSubcatAccessF = 0
and dsa.UserID = @UserID and dsa.SubcatID = @SubcatID
) access
)
Смысл: если пользователь имеет доступ на всю категорию (через DescendantSubcatAccess) — зачем проверять каждую задачу? Возвращаем ВСЕ задачи категории за один seek.
4.3 Разделение SubcatID IS NULL / IS NOT NULL¶
Каждый источник дублируется для двух режимов:
- @SubcatID IS NOT NULL — фильтр t.SubcatID = @SubcatID → seek по индексу
- @SubcatID IS NULL + @SubcatIDNotNullOnlyF = 0 — все категории → scan
Это разделение даёт оптимизатору чистые планы: при указании категории scan-ветви отсекаются условием @SubcatID is null = false.
5. Девять источников доступа¶
5.1 Категорийный доступ (all categories)¶
Маркер: --$$BEGIN SUBCAT_IS_NULL$$ (первый блок)
SELECT Tasks_cat.TaskID
FROM Tasks JOIN DescendantSubcatAccess dsa_all_cat
ON dsa_all_cat.SubcatID = Tasks_cat.SubcatID
AND dsa_all_cat.UserID = CASE WHEN @SubcatID IS NULL THEN @UserID END
WHERE @SubcatID IS NULL AND @SubcatIDNotNullOnlyF = 0
Работает только при @SubcatID IS NULL. При указании конкретной категории — CTE subcat_access решает быстрее.
Замещение: не нужно — заместители уже в DescendantSubcatAccess (через AssistanceSourceF).
5.2 Руководители заказчиков¶
Таблица: UserDirectors JOIN Tasks (по Tasks.UserID = заказчик)
Условия:
- settings.InheritAccessToDirectorsF = 1 (кодогенерация подставляет cast(0 as bit), условие = 1 → false → по умолчанию выключено)
- IsConfidential = 0 AND IsEncrypted = 0 — исключение приватных
- SubcatID <> ChatSubcatID — исключение чатов
Замещение: нет отдельной ветви (руководителей уже нет — функция отключена).
5.3 Руководители исполнителей¶
Таблица: TaskHelpers JOIN UserDirectors
Аналогично §5.2, плюс:
- LEFT JOIN vwTasksPrivate + tpr.TaskID IS NULL — исключение через indexed view
Замещение: нет.
5.4 Акцептанты подписей¶
Таблица: TaskSignatureUsers
Прямой доступ: tsut.UserID = @UserID
Замещение: через fn_GetUserAssistantsBySubcat → tsut.UserID = uass.UserId. Заместитель видит задачи принципала, ожидающие подписи (если не ExcludeSystemSubstitutes).
5.5 Групповые тэги (TagActions)¶
Таблица: vwUserGroupsTagActions JOIN Tasks
Условие: TagActionID = 1 (view).
Может быть отключено: --$$NEED_GROUP_TAG_ACTIONS and 1=0 — если SettingsCustom.ExcludeGroupsTagActions = 1.
Замещение: нет.
5.6 Смарт-доступ¶
Таблица: SmartAccessViewTasks (предвычисленный кэш)
Условие: savt.ActionID = 1
Замещение: через fn_GetUserAssistants → savt.UserID = uass.UserId + LEFT JOIN vwTasksPrivate (исключение приватных) + CategoryInclusionMode whitelist/blacklist.
5.7 Подписки индивидуальные (MailSubscribers)¶
Таблица: MailSubscribers
Прямой доступ: mss.UserID = @UserID
Замещение: через fn_GetUserAssistants + vwTasksPrivate + CategoryInclusionMode.
5.8 Подписки групповые¶
Таблица: vwGroupTaskSubscribers (indexed view)
Прямой доступ: ms.UserID = @UserID
Замещение: через fn_GetUserAssistantsBySubcat + vwTasksPrivate.
5.9 Гибкие права через ДП (ExtParamsRights)¶
Таблица: vwExtParamsRightsAll (view над ExtParamsRights + замещение)
Прямой доступ: epr.UserID = @UserID
Замещение: через fn_GetUserAssistants + vwTasksPrivate + CategoryInclusionMode.
5.10 Кастомные права¶
Таблица: CustomTaskPermissions
Прямой доступ: ctp.UserID = @UserID
Замещение: через fn_GetUserAssistants + vwTasksPrivate + CategoryInclusionMode.
6. Паттерн замещения¶
6 из 9 источников (§5.4, 5.6–5.10) имеют дополнительные ветви для замещения. Паттерн одинаковый:
--$$BEGIN ASSISTANTS$$
SELECT <TaskID>
FROM fn_GetUserAssistants(@UserID, 0, 0) uass
LEFT JOIN UserAssistantCategories uac
ON uac.AssistanceId = uass.AssistanceId AND uac.SubcatId = @SubcatID
JOIN <источник> ON <источник>.UserID = uass.UserId
LEFT JOIN vwTasksPrivate tpr ON tpr.TaskID = <источник>.TaskID
WHERE @WithoutAssistantsOnlyF = 0
AND tpr.TaskID IS NULL -- не конфиденциальная/зашифрованная
AND (
uass.CategoryInclusionMode = 2 AND uac.AssistanceId IS NULL -- blacklist ok
OR
uass.CategoryInclusionMode = 1 AND uac.AssistanceId IS NOT NULL -- whitelist ok
)
--$$END ASSISTANTS$$
vwTasksPrivate — indexed view с TaskID задач, где IsConfidential=1 OR IsEncrypted=1. LEFT JOIN + IS NULL = исключение этих задач из замещения.
Подробнее: substitution.md §6.
7. Обёртка: fn_CheckUserTaskPermissions¶
Проверка доступа к одной задаче:
fn_CheckUserTaskPermissions(@TaskID int, @UserID int, @SubcatID int)
RETURNS TABLE (enabled_f bit, UserID int, TaskID int)
Логика: SELECT TOP 1 ... FROM fn_UserTaskPermissionsGeneral(...) WHERE TaskID = @TaskID
8. Вызывающий код (C#)¶
TaskPermissionsService¶
Основной сервис проверки прав в бэкенде. Вызывает fn_CheckUserTaskPermissions или fn_UserTaskPermissionsGeneral через linq2db.
GridPermissionsService¶
При построении грида (ShowTasksFeed, ShowCommentsFeed) — добавляет CROSS APPLY fn_UserTaskPermissionsGeneral(...) к основному запросу. Фильтрует задачи на уровне SQL, до возврата данных.
SubcategoryPermissionsService¶
Проверяет доступ на уровне категории (не задачи) — через DescendantSubcatAccess напрямую, без вызова fn.
9. Производительность¶
Хинты¶
| Хинт | Где | Зачем |
|---|---|---|
NOLOCK |
Все таблицы | Допустима стейлость ради скорости |
FORCESEEK |
Tasks, DescendantSubcatAccess | Принудительный seek по PK/индексу |
NOEXPAND |
vwTasksPrivate, vwGroupTaskSubscribers | Принудительное использование индексов indexed view |
CTE subcat_access¶
Однократная проверка DescendantSubcatAccess — переиспользуется: если access_f=1, весь UNION ALL пропускается.
Conditional branches¶
@SubcatID IS NULL / IS NOT NULL — разделение позволяет SQL Server исключать целые ветви из плана при константном @SubcatID.
InheritAccessToDirectorsF = 0 (литерал)¶
Ветви руководителей (§5.2, 5.3) содержат cast(0 as bit) = 1 → compile-time false → оптимизатор исключает подзапросы полностью.
31 UNION ALL¶
Шаблон содержит 31 union all. Это не 31 логический источник — это 9 источников × варианты (SubcatID NULL/NOT NULL) × (прямой/замещение). SELECT DISTINCT на внешнем уровне убирает дублирование.
10. Две настройки руководительского доступа¶
| Настройка | Таблица | Влияет на |
|---|---|---|
InheritAccessToDirectorsF |
Settings |
fn_UserTaskPermissionsGeneral — ветви руководителей (через кодогенерацию) |
ForbidSubcatRightsInheritanceToDirector |
SettingsCustom |
DescendantSubcatAccessRefresh — наследование прав на категорию |
Разница: первая — видит ли руководитель задачи подчинённых. Вторая — наследует ли руководитель права на категорию (ViewAll).
С миграции v2.266: ForbidSubcatRightsInheritanceToDirector=true + InheritAccessToDirectorsF=0 — оба по умолчанию отключены.
11. Связанная документация¶
- business.md — 6 уровней доступа, 4 режима гибких прав
- database.md — таблицы, кэш, кодогенерация (§4, §10, §11)
- substitution.md — механизм замещения (паттерн в fn)
- impersonation.md — перевоплощение (меняет @UserID)
- backend.md — C# сервисы
12. Источники кода¶
| Объект | Файл | Строки |
|---|---|---|
| Шаблон функции | DB_MSSQL/dbo.fn_UserTaskPermissionsTemplate.UserDefinedFunction.sql |
664 |
| Генератор | DB_MSSQL/dbo.GenerateExtParamsRightsFunctions.StoredProcedure.sql |
— |
| fn_GetUserAssistants | DB_MSSQL/dbo.fn_GetUserAssistants.UserDefinedFunction.sql |
— |
| fn_GetUserAssistantsBySubcat | DB_MSSQL/dbo.fn_GetUserAssistantsBySubcat.UserDefinedFunction.sql |
— |
| fn_CheckUserTaskPermissions | DB_MSSQL/dbo.fn_CheckUserTaskPermissions.UserDefinedFunction.sql |
— |
| vwTasksPrivate | DB_MSSQL/dbo.vwTasksPrivate.View.sql |
— |
| SettingsUpdateHandler | core/TCClassLib/Settings/SettingsUpdateHandler.cs:132 |
— |
| EpRightsProcessorController | core/Valhalla.Admin/Categories/EpRightsProcessorController.cs |
— |
| TaskPermissionsService | core/TCClassLib/Permissions/TaskPermissionsService.cs |
— |
| GridPermissionsService | core/TCClassLib/Permissions/GridPermissionsService.cs |
— |