Перейти к содержанию

Проверка доступа к задачам — 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

  1. object_definition() → тело fn_UserTaskPermissionsTemplate
  2. Замена имени → fn_UserTaskPermissionsGeneral
  3. Чтение Settings.InheritAccessToDirectorsF, Settings.ChatSubcatID
  4. Text replace ссылок на литералы
  5. Обработка $$NEED_GROUP_TAG_ACTIONS
  6. DROP FUNCTION IF EXISTS fn_UserTaskPermissionsGeneral
  7. EXEC (@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_GetUserAssistantsBySubcattsut.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_GetUserAssistantssavt.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