# Синхронизация с Active Directory (AD Sync) — справочник

Синхронизация пользователей и групп из Active Directory / OpenLDAP в 1Форму. Односторонняя: AD → 1Форма. Справочник описывает, как устроена синхронизация, какие таблицы и настройки задействованы, какие настройки и endpoint'ы доступны администратору и как диагностировать проблемы.

Сервис подключения к Active Directory создаётся и настраивается в администрировании: указываются домен, учётная запись для доступа и признак «Домен является корнем леса».

![Настройка сервиса Active Directory в администрировании](https://help.1forma.ru/help-images/auth/ad_service_settings.png)

> **В сертифицированной сборке 1F Certificate Edition (ФСТЭК)** синхронизация с внешним каталогом не выполняется: задача ежедневного запуска не зарегистрирована, административные маршруты `api/admin/ldap`, `api/sync-ad`, `api/admin/users/{id}/sync-with-ad` и `api/admin/groups/domains` в сборке отсутствуют (ответ 404), синхронизация членства при входе не выполняется. Профили синхронизации и записи расписания при этом сохраняются в базе. Подробности — [раздел 13](#13-синхронизация-с-каталогом-в-1f-certificate-edition).

## Где настраивается

Все компоненты синхронизации настраиваются в администрировании, раздел **Подключения**:

| Что настроить | Где (Администрирование → Подключения) | Форма |
|---------------|----------------------------------------|-------|
| Подключение к каталогу (домен, учётная запись) | **Сервисы** → создать сервис типа Active Directory или OpenLDAP | `active-directory-service-settings` |
| Профиль синхронизации (привязка к сервису, фильтр OU) | **Синхронизации** | `synchronizations` |
| Флаги и параметры синхронизации (что синхронизировать, лимит, LDAP-фильтры) | **Синхронизации** → настройки профиля | `synchronization-settings` |
| Маппинг свойств AD → поля 1Ф | **Синхронизации** → вкладки «Общие настройки» и «Расширенные настройки пользователя» | — |
| Вход пользователей по учётным записям AD | **Провайдеры** → провайдер типа Active Directory | `authentication-providers` |

Порядок первичной настройки:

1. Создать **сервис** каталога (Подключения → Сервисы): тип Active Directory, домен, учётная запись для доступа.
2. Создать **профиль синхронизации** (Подключения → Синхронизации) и привязать его к сервису.
3. В **настройках профиля** включить нужные флаги (раздел 4), при необходимости задать фильтр OU, LDAP-фильтры и лимит обновлений.
4. Настроить **маппинг свойств** (раздел 5): сопоставить атрибуты AD полям пользователя 1Ф.
5. Запустить синхронизацию — вручную (кнопка в профиле или API из раздела 6) либо дождаться планового запуска задания `ADSyncJob` (ежедневно в 20:00).

## 1. Архитектура

Синхронизация может запускаться по расписанию, при логине, через смарт-действие или API; связь объектов AD и 1Ф ведётся по идентификатору SID.

### Точки запуска синхронизации

Синхронизация может запускаться из нескольких точек:

| Триггер | Что синхронизирует |
|---------|--------------------|
| По расписанию (ежедневно 20:00), задание `ADSyncJob` | Активные профили Active Directory и OpenLDAP: пользователи + группы + членство + вложенность. У OpenLDAP вложенность строится только для групп с членством по DN |
| При логине пользователя | Один пользователь (по запросу), может создать нового |
| Смарт-действие синхронизации | Один пользователь (по UserID) в каталоге Active Directory или OpenLDAP, может привязать идентификатор записи каталога |
| API (admin) — синхронизация пользователя | Один пользователь |
| API (admin) — синхронизация групп | Все группы |
| Создание пользователя администратором | Один пользователь, сразу после записи. Профиль ищется по имени службы каталога из `Users.DomainController`: сначала среди активных профилей OpenLDAP, затем среди профилей Active Directory |

> В 1F Certificate Edition ни один из перечисленных триггеров, кроме внутреннего вызова при создании пользователя, в сборке отсутствует — см. [раздел 13](#13-синхронизация-с-каталогом-в-1f-certificate-edition).

Связь между базой 1Ф и AD выполняется через **SID**: `Users.SID` = `objectSid` пользователя, `Groups.ADSID` = `objectSid` группы. Значение SID приходит из внешнего ввода — при регистрации и обновлении пользователя — и проверяется на формат: строка не вида `S-1-…` отвергается исключением, как в системном классе идентификатора Windows. В фильтры LDAP — при входе и при запросе статуса пользователя — SID подставляется экранированным, поэтому специальные символы в значении не искажают фильтр и не меняют условие поиска в каталоге. Для профиля OpenLDAP в те же поля пишется идентификатор записи каталога из схемы, по умолчанию `entryUUID`, а логин пользователя берётся из атрибута логина, по умолчанию `uid`.

**Автосинхронизация при создании пользователя.** Пользователь, созданный администратором, синхронизируется с каталогом сразу после записи, не дожидаясь планового прогона. Профиль выбирается по имени службы каталога из `Users.DomainController` (в карточке пользователя — «Домен пользователя»): у пользователя OpenLDAP это имя службы, у пользователя Active Directory — домен. Для пользователя OpenLDAP имя службы записывает само ядро, когда в карточке выбран провайдер аутентификации OpenLDAP. Синхронизация запускается при включённых флагах профиля «Данные пользователей» или «Членство пользователей» вместе с «Синхронизировать существующих пользователей», а для профиля OpenLDAP — только при создании из административного интерфейса: при создании смарт-действием или импортом пользователь OpenLDAP автоматически не синхронизируется. Если имя службы каталога не заполнено, синхронизация пропускается, а пользователь создаётся. Если активных профилей OpenLDAP с этим именем несколько, синхронизация тоже пропускается, причина пишется в журнал ошибок, и создание пользователя не прерывается.

В синхронизацию попадают:

- Пользователи — с непустым `SID` и не уволенные (`IsFired_2 = false`).
- Группы — с непустым `ADSID` и принадлежащие текущему клиенту (`CustomerID`).

## 2. ADSyncJob — порядок выполнения

Плановая синхронизация выполняется заданием `ADSyncJob` со следующими параметрами:

- Расписание: ежедневно в 20:00.
- Параллельный запуск запрещён (защита через OS Mutex `Global\ADSyncJob`) — ручной и автоматический синк не пересекаются.
- Таймаут транзакции: 300 минут.
- Контекст выполнения: системный робот.

> **В 1F Certificate Edition задача в сборку не входит** и ежедневного запуска в 20:00 не происходит. Диагностический запрос `QRTZ_FIRED_TRIGGERS` из раздела 11 в профиле всегда пуст.

**Ошибки при обходе каталога.** Если объект, найденный поиском в каталоге, при чтении отвечает ошибкой «There is no such object on the server» (`0x80072030`), он пропускается: в журнал пишется предупреждение «Пропущен недоступный объект каталога» с его `distinguishedName`, и обход продолжается. Другие ошибки каталога не пропускаются. Ошибка синхронизации одного профиля записывается в журнал и не останавливает остальные профили; если хотя бы один профиль завершился ошибкой, задание после обработки всех профилей завершается ошибкой и запуск не считается успешным.

### Шаги синхронизации

Шаги выполняются в следующем порядке; каждый управляется своим флагом профиля (`SynchronizationProfilesADSettings`):

| # | Шаг | Флаг (SyncSettings) |
|---|-----|---------------------|
| 1 | Создание отсутствующих групп | `SyncGroupCreation && SyncExistingUsers` |
| 2 | Создание новых пользователей из AD | `SyncUserDataBySchedule` |
| 3 | Синхронизация данных групп (SID, DisplayName, Domain) | `SyncGroupData && SyncExistingUsers` |
| 4 | Синхронизация данных пользователей (по маппингу) | `SyncUserData && SyncExistingUsers` |
| 5 | Синхронизация вложенности групп (`GroupParents`) | `SyncGroupNesting && SyncExistingUsers` |
| 6 | Синхронизация членства user-group | `SyncUserGroupMembership && SyncExistingUsers` |
| 7 | Нормализация членства (хранимая процедура) | `SyncUserGroupMembership && SyncExistingUsers` |
| 8 | Проверка лимита строк | `MaximumADSyncRows` |
| 9 | Перезагрузка кэша пользователей | всегда |
| 10 | Смарт-события для созданных пользователей | для новых пользователей |
| 11 | Смарт-события для обновлённых пользователей | для изменённых |

### Что делает каждый шаг

Подробнее о том, какие таблицы затрагивает каждый шаг:

| Шаг | Что делает |
|-----|-----------|
| Создание отсутствующих групп | Читает маски из `ADGroupSyncMasks`, находит группы в AD, создаёт в `Groups` с заполненным `ADSID` |
| Синхронизация данных групп | Обновляет `ADSID`, `DisplayName`, `Domain` у существующих групп |
| Вложенность групп | Синхронизирует `GroupParents` (parent-child связи групп) |
| Членство user-group | Синхронизирует `UserGroupsActual` (прямое членство пользователь-группа) |
| Данные пользователей | Обновляет поля пользователей по маппингу `ADPropertyMapping`; для изменённых — локализованные ФИО и пересборка отображаемых имён |
| Создание пользователей | Создаёт новых пользователей (фильтр по OU и `UserAccountControl`) |
| Синхронизация одного пользователя | Nick, SID, mapped-свойства, ext-свойства, оргструктура |

**Пересборка отображаемых имён при смене ФИО.** Синхронизация обновляет не только поля `Users`, но и подписи пользователя. Для каждого изменённого пользователя после сохранения записываются локализованные ФИО (`UserHelpersProcedures.UpdateUserLocalizationValues`), а затем пересобираются отображаемые имена в `dbo.UserNameModes` — по каждому языку пользователя, хранимой процедурой `dbo.RefreshUserNameModes`. Порядок обязателен: для языка с заведённой локализацией имя берётся именно из неё (в `dbo.fn_UserAllDisplayNames` — `isnull(username.lastName, u.LastName)`), и перестановка шагов вернула бы прежнюю подпись. Такой порядок действует и на быстром пути синхронизации, и на обычном; пересборка выполняется по каждому изменённому пользователю. Сразу после пересборки сбрасывается кэш отображаемых имён, поэтому новое ФИО сразу видно в подписях и поиске, без ожидания истечения кэша.

### Группы OpenLDAP

Для профиля OpenLDAP плановое задание выполняет те же шаги групп — создание, данные, членство, вложенность — по тем же флагам профиля, что и для Active Directory. Классы групп и атрибут членства задаёт схема каталога сервиса (`directorySchema`, см. раздел 7); по умолчанию это `groupOfNames` с членством по различительному имени в `member` и `posixGroup` с членством по логину в `memberUid`. Логин из `memberUid` переводится в DN по пользователям каталога, поэтому для OpenLDAP кеш пользователей готовится раньше групп, а логин, не найденный среди пользователей каталога, в членство не попадает. Вложенность групп строится только из членства по DN: в `memberUid` лежат логины пользователей, а не групп.

Группа OpenLDAP связывается с группой 1Формы полем `Groups.ADSID` — идентификатором записи каталога, а в `Groups.Domain` пишется постоянный ключ службы `openldap:{guid}` из Guid настроек сервиса (`DirectoryServiceKey`), а не редактируемое описание сервиса. Группа без имени или идентификатора и группа с неизвестным видом членства пропускаются с предупреждением в журнале; если ключ службы определить не удалось, группы профиля не синхронизируются. Синхронизация пользователей профиля в обоих случаях продолжается.

**Чтение каталога по схеме.** Профиль OpenLDAP читает каталог нейтральными сущностями пользователя и группы поверх общего запросного слоя; путь Active Directory со статическим соответствием атрибутов не затронут. Пользователи читаются фильтром по классу из схемы, атрибуты запрашиваются явным списком: класс, различительное имя, атрибут логина и атрибут идентификатора. Явный список нужен потому, что идентификатор записи — служебный атрибут, и по выборке «все атрибуты» каталог его не отдаёт. Группы всех классов схемы читаются одним запросом: для одного класса это обычный фильтр по классу, для нескольких — объединение через «или». Разбирается запись по фактическому классу из её же атрибутов; если у записи несколько классов из схемы, берётся первый подходящий в порядке схемы. Атрибуты, которых каталог не вернул, остаются пустыми, запись с классом вне схемы не интерпретируется, а ненастроенная или неполная схема приводит к ошибке ещё до запроса в каталог.

**Пользователи OpenLDAP.** Логин учётной записи берётся из атрибута логина схемы, а в `Users.SID` пишется идентификатор записи каталога — строковый, а не двоичный Windows-SID. Домен у профиля OpenLDAP не вычисляется, поэтому флаг добавления домена к логину на него не действует и доменный префикс не подставляется. Проверка формата Windows-SID выполняется только для профилей Active Directory: учётную запись каталога OpenLDAP можно создать и отредактировать вручную, идентификатор в виде строки ошибки не вызывает. Если администратор создал такого пользователя без идентификатора, он подставляется по логину — поиском записи каталога с тем же логином.

**Ручные входы по пользователям и подразделениям.** Импорт пользователей из каталога в администрировании, дерево подразделений каталога в настройках профиля, привязка существующего пользователя к записи каталога и синхронизация одного пользователя из карточки строят подключение по типу профиля и читают объекты по схеме каталога, поэтому доменный контроллер профилю OpenLDAP не нужен. Для синхронизации одного пользователя профиль выбирается по `Users.DomainController`: при заполненном имени службы берётся активный профиль OpenLDAP с таким именем, при нескольких одноимённых активных профилях вход отвечает ошибкой выбора, а если профиля OpenLDAP с таким именем нет — ищется профиль Active Directory. При пустом `Users.DomainController` берётся единственный активный профиль OpenLDAP, а если их несколько, вход также отвечает ошибкой выбора. Имя службы каталога записывается в `Users.DomainController` при синхронизации пользователя.

## 3. База данных

Синхронизация опирается на две группы таблиц: служебные таблицы профилей и настроек и AD-поля в основных таблицах пользователей и групп.

### Таблицы профилей и настроек

Профили синхронизации и их параметры хранятся в связанных таблицах:

```
ServicesSettings (ServiceType=ActiveDirectory|OpenLDAP)
  ├── LDAPServicesCredentials (FK ServiceId)
  ├── OpenLDAPServicesCredentials (FK ServiceId)
  └── SynchronizationProfiles (FK ServiceId, UNIQUE)
        ├── SynchronizationProfilesADSettings (FK SynchronizationProfileId, 1:1)
        │     ├── SynchronizationProfilesADOrgUnits (FK SynchronizationProfileADSettingsId)
        │     └── ADGroupSyncMasks (FK SynchronizationProfileADSettingsId)
        └── ADPropertyMapping (FK SynchronizationProfileId)
```

| Таблица | Назначение |
|---------|-----------|
| `ServicesSettings` | Реестр внешних сервисов. `ServiceType` = ActiveDirectory, OpenLDAP, SAML, OAuth, Radius |
| `LDAPServicesCredentials` | Домен, логин, пароль (зашифрован), `IsDomainHasForestState` |
| `OpenLDAPServicesCredentials` | Аналогично для OpenLDAP |
| `SynchronizationProfiles` | Профиль: `IsActive`, `SyncType`, `ServiceId`, `CustomerId` |
| `SynchronizationProfilesADSettings` | Все флаги синхронизации (per-profile) |
| `SynchronizationProfilesADOrgUnits` | Фильтр по OU |
| `ADPropertyMapping` | Маппинг: `OfProperty` (поле 1Ф) → `AdProperty` (атрибут AD), per-profile |
| `ADGroupSyncMasks` | Шаблоны имён (wildcards) для автосоздания групп |

### AD-поля в основных таблицах и таблицы, затрагиваемые синхронизацией

AD-идентификаторы хранятся в основных таблицах пользователей и групп:

| Таблица | Колонки | Назначение |
|---------|---------|-----------|
| `Users` | `SID` (VARCHAR 200, index `IX_Users_SID`), `DomainController` | Идентификатор пользователя в AD |
| `Groups` | `ADSID` (VARCHAR 8000), `EnableADSync` (computed: `ADSID IS NOT NULL AND LEN > 0`), `Domain` | Идентификатор группы в AD |
| `UserADNickHistory` | `UserID`, history data | История смены AD-логинов |

В ходе синхронизации записи создаются и обновляются в следующих таблицах:

| Операция | Таблица |
|----------|---------|
| Создание групп | `Groups` |
| Обновление групп | `Groups` |
| Вложенность | `GroupParents` |
| Членство | `UserGroupsActual` |
| Обновление пользователей | `Users` |
| Создание пользователей | `Users` |
| Ext-свойства | `UserInfoExtValues` |

### Хранимые процедуры и алгоритм tc_NormalizeGroupUsersMembership

В синхронизации задействованы хранимые процедуры (колонка PG — поддержка на PostgreSQL):

| SP | Назначение | PG |
|----|-----------|-----|
| `tc_NormalizeGroupUsersMembership` | Рекурсивный CTE по `GroupParents` → нормализует `UserGroups` и `GroupUsersMembership` с учётом вложенности | **Нет** |
| `sp_SyncAutoManagedGroup` | Синхронизация автоуправляемых групп | ? |
| `RefreshUserNames` | Обновление кэша имён пользователей | Да |
| `RefreshUserNameModes` | Пересборка отображаемых имён одного пользователя по всем его языкам | Да |

Процедура нормализует членство с учётом вложенности групп в четыре шага:

1. Вставляет отсутствующие записи в `GroupUsersMembership` из `UserGroups`
2. CTE `NestingMembershipProjection` — рекурсивный обход `GroupParents`, добавляет membership с учётом вложенности
3. Закрывает устаревшие записи membership (`EndDate`)
4. Синхронизирует `UserGroups`: удаляет не подтверждённые через вложенность, добавляет новые

Механизм записи на MS SQL и PostgreSQL различается — см. раздел 8.

### Настройки в таблице Settings (устаревшие, мигрированы в профили)

Раньше настройки синхронизации хранились в общей таблице `Settings`. Сейчас они перенесены в профили; таблица соответствия:

| Колонка | По умолчанию | Мигрирована в |
|---------|---------|---------------|
| `MaximumADSyncRows` | 10 | `SynchronizationProfilesADSettings.MaximumADSyncRows` |
| `ADSyncGroupCreation` | true | `.SyncGroupCreation` |
| `ADSyncGroupData` | true | `.SyncGroupData` |
| `ADSyncGroupNesting` | true | `.SyncGroupNesting` |
| `ADSyncUserData` | true | `.SyncUserData` |
| `ADSyncUserGroupMembership` | true | `.SyncUserGroupMembership` |
| `ADSyncOrganizationUnits` | NULL | `.SynchronizationProfilesADOrgUnits` |
| `ADSyncUserDataBySchedule` | false | `.SyncUserDataBySchedule` |
| `IncludeDomainInNick` | — | `.IncludeDomainInNick` |
| `DomainController` | NULL | `LDAPServicesCredentials.DomainController` |
| `DomainControllerUser` | NULL | `LDAPServicesCredentials.DomainControllerUser` |
| `DomainControllerPassword` | NULL | `LDAPServicesCredentials.DomainControllerPassword` |

**Не путать с процедурой пересчёта состава.** `dbo.tc_NormalizeGroupUsersMembership` нормализует членство при AD-синхронизации: на MSSQL это шаг задания `ADSyncJob`, на PostgreSQL такой процедуры нет. Пересчёт состава группы — отдельная процедура `dbo.tc_UpdateGroupMembership`: её вызывают из кода при изменении состава, она работает на обеих СУБД, и её рекурсивный обход `dbo.GroupParents` защищён от цикла. У нормализации такой защиты нет: глубину обхода ограничивает обрыв ошибкой на глубине 100 (без `maxrecursion 0`), а на PostgreSQL процедура просто не выполняется. Поэтому цикл в иерархии больше не подвешивает пересчёт состава, но нормализацию членства это не защищает.

## 4. Флаги настроек (SynchronizationProfilesADSettings)

Поведение синхронизации задаётся флагами профиля. Настраиваются в администрировании: **Подключения → Синхронизации → настройки профиля** (форма `synchronization-settings`).

| Флаг | Тип | Описание |
|------|-----|---------|
| `SyncExistingUsers` | bool | **Главный выключатель.** Все шаги (кроме 2) требуют true |
| `SyncUserData` | bool | Синхронизировать данные пользователей (шаг 4) |
| `SyncGroupData` | bool | Синхронизировать данные групп (шаг 3) |
| `SyncGroupCreation` | bool | Создавать отсутствующие группы (шаг 1) |
| `SyncUserGroupMembership` | bool | Синхронизировать членство user-group (шаги 6, 7) |
| `SyncGroupNesting` | bool | Синхронизировать вложенность групп (шаг 5) |
| `CreateUsers` | bool | Разрешить создание пользователей при логине |
| `SyncUserDataBySchedule` | bool | Создавать новых пользователей при плановой синхронизации (шаг 2) |
| `SyncOnlyADEntity` | bool | Синхронизировать только AD-сущности в оргструктуре |
| `IncludeDomainInNick` | bool | Добавлять домен к нику (DOMAIN\user) |
| `MaximumADSyncRows` | int? | Лимит строк (защита от массового повреждения). По умолчанию в Settings = 10 |
| `UsersADFilter` | string | LDAP-фильтр пользователей (max 1000 символов) |
| `GroupsADFilter` | string | LDAP-фильтр групп |
| `ChangePasswordException` | string | Исключения смены пароля |
| `EwsServiceSettingsId` | int? | FK на EWS-настройки (Exchange) |

## 5. Маппинг свойств AD → 1Ф

Маппинг определяет, какие атрибуты Active Directory переносятся в поля пользователя 1Ф. Настраивается в профиле синхронизации (**Подключения → Синхронизации**) на вкладках «Общие настройки» и «Расширенные настройки пользователя».

### Блоки маппинга (UI)

В интерфейсе настройки маппинга поля сгруппированы в блоки:

| Блок | Поля 1Ф |
|------|---------|
| Рабочая информация | Динамические типы оргединиц (`OrgUnitType{Id}`) |
| Личные данные | Nick, LastName, FirstName, MiddleName, BirthDate, DisplayName, ImgAvatar, MaidenName, EnglishDisplayName, UserText |
| География | Country, City, Room |
| Контакты | Phone, Phone2, Phone3, CellPhone, HomePhone, Fax, Email, ExternalEmail, ICQ (Telegram), Skype, LiveJournal (—), Twitter (WhatsApp), SIP, TimeZone |
| Функционал | BusinessFunctions |
| Прочее | Notes |

### Стандартные AD-атрибуты и расширенные свойства (UserInfoExt)

Стандартный набор атрибутов AD, доступных для сопоставления:

```
assistant, comment, company, department, description, displayName, division,
givenName, homePhone, mail, manager, mobile, sn, telephoneNumber, otherTelephone,
title, userPrincipalName, physicalDeliveryOfficeName, co, l
```

Для каждого расширенного свойства пользователя (`UserInfoExt`) с заполненным `AdProperty` значение копируется из AD в `UserInfoExtValues`. Настраивается через вкладку «Расширенные настройки пользователя» в UI синхронизации.

## 6. Синхронизация аватара пользователя

При импорте thumbnailPhoto (или иного binary-атрибута фото по маппингу) сохраняется в `Users.AvatarFileId` через `UserAvatarService.SyncAvatarFromAd`.

**Дедупликация по хешу.** Считается SHA-256 исходных байт AD-фото (`HashHelper.ComputeSHA256Hex`), результат хранится в системном расширенном свойстве `ADSyncAvatarHash` (`UserInfoExt`/`UserInfoExtValue`). Если хеш совпадает с сохранённым **И** у пользователя есть `AvatarFileId` — sync ничего не делает: не перезаливает файл, не бампает `LastPersonalInfoUpdateTime`, не инвалидирует кеши файлов.

**Восстановление после ручного удаления.** При отсутствии `AvatarFileId` (пользователь удалил аватар вручную) фото восстанавливается из AD даже при совпадении хеша.

**Multi-instance nuance.** Текущий `AvatarFileId` берётся из БД (`UsersEntityService`), не из `UsersCache` — иначе кеш может отставать в кластере.

### Версионирование файла аватара

Новое фото из AD заливается **новой версией существующего файла**, а не отдельным файлом: `Users.AvatarFileId` не меняется, `LatestVersionId` увеличивается на единицу, предыдущая версия остаётся в истории файла. Ранее выданные ссылки вида `/api/files/download/{fileId}/{versionId}` продолжают отдавать своё изображение. Новый файл создаётся только если у пользователя ещё нет аватара либо `AvatarFileId` указывает на удалённый файл (`File.IsExist`).

### Приоритет AD над ручным аватаром

Дедупликация врезана в ветку синхронизации, а не в `InsertPersonImageAvatar` — тот же метод используется ручной загрузкой из профиля и мобильного приложения. При ручной загрузке аватара и при его удалении сохранённый `ADSyncAvatarHash` **сбрасывается**, поэтому следующая синхронизация не попадает в пропуск и применяет фото из AD.

> **AD — источник истины.** Если у пользователя в AD есть фото, ручная замена аватара в 1Ф будет перезаписана при следующей синхронизации. Чтобы пользователи могли ставить свои аватары, снимите маппинг свойства фото в профиле синхронизации.

### Поведение по сценариям

| Сценарий | Результат |
|----------|-----------|
| Фото в AD не менялось, аватар есть | Пропуск: файл, версия, `LastPersonalInfoUpdateTime`, хеш не меняются |
| Фото в AD заменено | `AvatarFileId` прежний, `LatestVersionId` +1, хеш обновлён |
| У пользователя нет аватара | Создаётся новый файл, заполняются `AvatarFileId` и хеш |
| Аватар удалён вручную | Хеш сброшен, `AvatarFileId` пуст — синк восстанавливает фото из AD |
| Аватар заменён вручную | Хеш сброшен — синк создаёт новую версию и перезатирает ручное фото из AD |
| Фото в AD отсутствует | `SyncAvatarFromAd` не вызывается, текущий аватар остаётся |

## 7. REST API

Администрирование синхронизации доступно через REST API. Ниже — основные группы endpoint'ов.

### Профили синхронизации — `/api/sync-ad` (admin)

Управление настройками профиля, навигацией по дереву AD и связыванием пользователей:

| HTTP | Route | Назначение |
|------|-------|-----------|
| GET | `profiles/{id}/settings` | Получить настройки профиля |
| POST | `profiles/{id}/settings/update` | Обновить настройки профиля |
| GET | `profiles/{id}/domain/folders-tree` | Дерево OU домена |
| GET | `profiles/{id}/domain/root-folder` | Корневые папки домена |
| POST | `profiles/{id}/domain/folder` | Загрузить папку дерева |
| POST | `profiles/{id}/domain/unload-users` | Выгрузить пользователей |
| POST | `profiles/{id}/users-for-link` | Пользователи для связывания |
| POST | `link-users` | Связать пользователей с AD |
| GET | `ad-properties-names` | Список AD-атрибутов |
| GET | `profiles/{id}/sync-properties/blocks` | Блоки маппинга свойств |
| POST | `profiles/{id}/sync-properties/update` | Обновить маппинг свойств |
| GET | `extra-sync-properties` | Расширенные свойства синка |
| POST | `extra-sync-properties/update` | Обновить расширенное свойство |

**Лес доменов.** Если у сервиса Active Directory установлен признак «Домен является корнем леса», `domain/root-folder` возвращает все домены леса, а раскрытие дерева и выгрузка идут по каждому домену отдельно: к дочернему домену открывается собственное подключение по его DNS-имени, переход по рефералам в режиме леса выключен. Поэтому DNS-имя каждого домена леса должно резолвиться и быть доступно с сервера приложения. Домен, отключённый в таблице использования доменов (`DomainUsages`), в обход не попадает; ошибка на одном домене леса пишется в журнал и не прерывает обход остальных.

**Тип службы и схема каталога в настройках профиля.** Настройки профиля (`profiles/{id}/settings`) содержат два поля сверх прежнего набора. `serviceType` — тип службы каталогов; сервер вычисляет его по сервису профиля и из запроса не применяет. Поле nullable намеренно: у перечисления типов нет члена со значением 0, поэтому незаполненное не-nullable поле уходило клиенту числом `0` — значением вне перечисления, — тогда как метод настроек отдаёт его именем. Теперь незаполненное приходит как `null`, а заполненное всегда именем. `directorySchema` — схема каталога OpenLDAP; на чтении заполняется только для профилей OpenLDAP, у Active Directory остаётся пустой. Схема хранится в `SettingsCustom` по ключу сервиса: если значения нет, отдаётся набор по умолчанию, если сохранённое значение не разбирается — метод завершается ошибкой. На сохранении схема принимается только для сервиса OpenLDAP, проверяется на корректность и записывается через настройки этого сервиса, после успешной транзакции кэш схемы сбрасывается.

**Страница настроек профиля в SPA.** Форма настроек синхронизации (`sync-ad-settings`) определяет тип службы по настройкам профиля и для профиля OpenLDAP прячет всё AD-специфичное: кнопки «Выгрузить из AD» и «Связь с AD», флаги «Логины при синке с AD сохранять в формате Domain\Nick», «Только орг. единиц из Active Directory» и «Маска для создания групп из AD», дерево организационных единиц и селект «Настройки EWS». Дерево OU для такого профиля не запрашивается вовсе (`domain/folders-tree` не вызывается), а при сохранении список организационных единиц уходит пустым. Подписи остальных полей читаются в «каталожной» редакции: «Синхронизация с каталогом», «Создавать пользователей из каталога», «Фильтр пользователей каталога (LDAP)», «Фильтр групп каталога (LDAP)», «Максимально допустимое количество обновлений при синхронизации с каталогом», «Информация о требуемой сложности пароля в каталоге». У профиля Active Directory ничего из перечисленного не скрывается: форма выводится полностью, подписи полей — обычные, не каталожные.

**Схема каталога в форме настроек.** Вместо скрытых блоков у профиля OpenLDAP появляется форма схемы каталога: у пользователей — `objectClass`, атрибут логина и атрибут идентификатора; у каждой группы — `objectClass`, атрибут названия, атрибут идентификатора и членство (атрибут и тип значения — `dn` или `uid`). Групп может быть несколько: строки добавляются и удаляются, но список не может быть пустым и последнюю строку удалить нельзя. Сохранение у профиля OpenLDAP требует валидной схемы помимо основной формы; у Active Directory схема в запрос не попадает. Схема из настроек профиля подставляется в форму как есть; если поле схемы в ответе пустое, форма заполняется набором по умолчанию — пользователи `inetOrgPerson` / `uid` / `entryUUID`, группы `groupOfNames` (членство `member`, тип `dn`) и `posixGroup` (членство `memberUid`, тип `uid`).

**Недоступный объект в дереве OU.** При раскрытии узла дерева OU в настройках профиля Active Directory дочерний объект, который при чтении отвечает ошибкой «There is no such object on the server» (`0x80072030`), пропускается: в журнал пишется предупреждение «Пропущен недоступный объект каталога» с его LDAP-путём, остальные дочерние объекты узла отображаются. Другие ошибки каталога прерывают раскрытие узла.

**Раскрытие узла дерева и предел ожидания.** Запросы `domain/folders-tree`, `domain/root-folder` и `domain/folder` (раскрытие узла) идут в каталог под общим пределом ожидания операции — см. [8.1](#81-предел-ожидания-операций-с-каталогом). При превышении вместо ожидания лимита прокси приложение возвращает собственную ошибку с указанием хоста, базы поиска, области и длительности и пишет запись в журнал.

На площадке под Windows (IIS) потомки узла читаются одним поиском уровня узла с фильтром классов и только теми атрибутами, которые нужны дереву, — раньше для этого читались все свойства каждого потомка. Следствие: раскрытие узла заметно быстрее на каталогах с большим числом объектов, но состав узлов может отличаться от прежнего — часть служебных контейнеров в дереве не показывается. Это ожидаемое поведение, а не потеря данных: объекты по-прежнему читаются при синхронизации и выгрузке.

**Дерево каталога профиля OpenLDAP.** Для профиля OpenLDAP `profiles/{id}/domain/folders-tree` и `profiles/{id}/domain/folder` отбирают узлы по объектным классам схемы каталога профиля, а не по фиксированным классам Active Directory: учитываются корневые контейнеры каталога, подразделения и класс учётной записи из схемы. Поэтому в дереве видны и подразделения, и записи пользователей, а отдельного пользователя OpenLDAP можно отметить и выгрузить точечно — так же, как в дереве Active Directory.

### Поиск в LDAP, прочие endpoints и MCP-серверы

Поиск пользователей и групп непосредственно в каталоге LDAP:

| HTTP | Route | Назначение |
|------|-------|-----------|
| GET | `providers` | Активные профили AD-синхронизации |
| POST | `search-users` | Поиск пользователя в LDAP |
| POST | `search-groups` | Поиск групп в LDAP |

Точечная синхронизация и управление настройками сервиса:

| HTTP | Route | Назначение |
|------|-------|-----------|
| POST | `/api/admin/users/{userId}/sync-with-ad` | Синхронизировать одного пользователя |
| GET | `/api/admin/groups/domains` | Список доменов групп |
| POST | `/api/admin/groups/ad/sync-all-groups` | Синхронизировать группы всех активных профилей — Active Directory и OpenLDAP |
| CRUD | `/api/admin/services-settings/{id}` | Управление настройками сервиса (GET/POST/PUT/DELETE) |

> **Недоступно в 1F Certificate Edition (ФСТЭК-сборка `UniForm.Barebone`).** Группы `api/admin/ldap` (`providers`, `search-users`, `search-groups`), `api/sync-ad` (13 методов), маршрут `POST /api/admin/users/{userId}/sync-with-ad` и `GET /api/admin/groups/domains` в сборку не входят: обращения отвечают 404. Маршрут `POST /api/admin/groups/ad/sync-all-groups` в этой части не менялся.

**Ручная синхронизация групп и поиск групп.** `sync-all-groups` проходит по всем активным профилям синхронизации — Active Directory и OpenLDAP: создаёт отсутствующие группы и синхронизирует членство пользователей по флагам профиля, вложенность групп — всегда; шаг данных групп в ручной синхронизации не выполняется. Каждый профиль обрабатывается в собственном скоупе, транзакции и контексте базы, поэтому сбой одного профиля пишется в журнал ошибок и не прерывает остальные; после обхода запрос завершается ошибкой с номерами упавших профилей: «Не удалось синхронизировать группы по профилям синхронизации: … Профили без ошибок синхронизированы, подробности — в журнале ошибок.».

Для профиля OpenLDAP `groups/domains` и `ldap/providers` отдают в поле домена ключ службы `openldap:{guid}` (у Active Directory — контроллер домена), а `ldap/search-groups` ищет группу по схеме каталога без учёта регистра имени (у Active Directory — точное совпадение отображаемого имени). При входе пользователя OpenLDAP его членство в группах синхронизируется по тем же флагам, что у Active Directory: «Членство пользователей» при включённой синхронизации существующих пользователей.

Операции синхронизации также доступны через MCP-серверы:

| Сервер | Тулов | Доступ |
|--------|-------|--------|
| `mcp-admin-api-ldap` | 3 | admin |
| `mcp-user-api-v2-sync-ad` | 13 | user |
| `mcp-admin-api-services-settings` | 6 | admin |

> **MCP в 1F Certificate Edition не поднимается**. Дополнительно пакеты `mcp-admin-api-ldap` и `mcp-user-api-v2-sync-ad` не регистрируются в любом случае: их инструменты строятся по снятым контроллерам `LdapController` и `SyncAdController`.

Смарт-действие синхронизации: `action_sync_user_with_a_d` — параметры: `param_0` (UserID), `param_1` (SynchronizationProfileADDto, optional). Действие работает и с пользователями OpenLDAP: профиль берётся из переданного параметра, иначе ищется по имени службы каталога из `Users.DomainController`, а при незаполненном имени — по единственному активному профилю OpenLDAP.

## 8. Конфигурация

Дополнительные параметры синхронизации задаются в конфигурации приложения:

| Источник | Ключ | Описание |
|----------|------|---------|
| `Configuration` | `UsePostgreSQLDatabase` | Переключатель MSSQL/PG |
| `Configuration` | `ExcludeAdSubdomains` | Исключённые поддомены при навигации по дереву AD |
| `Configuration` | `SystemRobotId` | ID робота для контекста выполнения ADSyncJob |
| `web.config` | `ActiveDirectoryAuthenticationMode` | Режим аутентификации: `DirectoryServices` (по умолчанию) или `PrincipalContext` (для одноимённых учёток в лесе) |
| SettingsCustom | `LDAP_AdGlobalCatalogHosts` | Оптимизация: список GC-хостов для ускорения загрузки дерева AD |
| SettingsCustom | `LDAP_OperationTimeoutSeconds` | Предел ожидания одной операции с каталогом в секундах. Основной ключ, применяется к Active Directory и OpenLDAP |
| SettingsCustom | `DirectoryServices_OperationTimeoutSeconds` | Резервный ключ предела ожидания. Используется, только если основной не задан или заданы ноль и отрицательное значение |

### 8.1. Предел ожидания операций с каталогом

Каждая операция с каталогом — подключение, привязка, поиск, чтение страниц результата, получение корневых папок и доменов леса, раскрытие узла дерева подразделений — имеет собственный предел ожидания. Он нужен, чтобы медленный или молчащий каталог не оставлял запрос открытым до срабатывания лимита прокси: раньше такой запрос заканчивался на стороне прокси ошибкой `504`, и в журнале площадки не оставалось ни одной записи.

**Откуда берётся значение**

- настройка площадки `LDAP_OperationTimeoutSeconds` (в секундах) — основная;
- настройка `DirectoryServices_OperationTimeoutSeconds` — резервная, берётся, только если основная не задана или задана нулём и отрицательным значением;
- если не задана ни одна из настроек, действует значение из кода — **25 секунд**.

Значение берётся один раз на операцию, поэтому менять настройку имеет смысл до начала работы с деревом. Рабочий диапазон — 20–30 секунд: меньше 20 секунд на живом каталоге даёт ложные отказы, больше 50 секунд снова приближает ответ к лимиту прокси.

**Что под пределом, а что нет.** Предел 25 секунд действует на интерактивное раскрытие дерева подразделений и на обход доменов леса: если контроллер домена не ответил за это время, операция прерывается своей ошибкой с хостом, базой поиска и длительностью — вместо ошибки `504` от прокси. Зависшие запросы изолируются раздельно по каждому хосту каталога, поэтому сбой одного контроллера не блокирует навигацию по остальным доменам. Массовые поиски — синхронизация пользователей AD, выгрузка пользователей и проверка SID — предел не применяют: их результаты проверяются на полноту пагинации, поэтому неполный ответ каталога не даёт частичную загрузку или ошибочную перезапись данных.

Политика ожидания выбирается по операции, а не по способу вызова. Предел применяется там, где пользователь ждёт ответа интерактивно, — это раскрытие дерева подразделений и обход доменов леса; массовые операции — синхронизация пользователей AD, выгрузка пользователей и проверка SID — идут по общему бюджету, без предела 25 секунд, даже когда внутри используется тот же примитив чтения узла каталога. Поэтому выгрузка на медленном, но отвечающем каталоге не завершается превышением ожидания.

**Общий бюджет составной операции.** Раскрытие узла дерева, построение дерева подразделений, получение корневых папок и обход доменов леса выполняются пошагово, и все шаги одной операции делят один бюджет времени: каждый следующий шаг получает остаток общего предела, а не новый предел заново. Обращения к базе 1Формы (чтение профиля синхронизации, выборка SID и логинов) в бюджет каталога не входят.

**Что происходит при превышении.** Операция завершается собственной ошибкой, а не зависает: пользователь видит сообщение о превышении предела ожидания с указанием хоста, базы поиска, области и длительности, а в журнал приложения пишется запись уровня предупреждение с теми же данными. Ошибка приходит и тогда, когда предел сработал на стороне каталога или сетевого стека: признаки нативного таймаута приводятся к той же собственной ошибке. Обычные ошибки каталога (нет прав, неверные учётные данные, недоступный объект) остаются своими и превышением ожидания не считаются.

**Неполная выдача — отдельный результат.** Если каталог вернул серверный предел выборки, отказала одна страница домена леса или сработал нативный таймаут при чтении страниц, операция не выдаёт урезанный список как полный: результат помечается признаком неполноты, а когда исчерпан весь лес и списка нет вовсе — завершается ошибкой. Отказ в доступе и любой другой сбой каталога, не связанный со временем, превышением ожидания не считается: в ошибке и в журнале остаётся исходная причина.

**Параллельные обращения без нативного предела.** Часть операций — получение корней и древовидные нативные вызовы — своего нативного предела не имеет, они выполняются не более двух одновременно на процесс приложения. Если свободное место не освободилось за отведённое время, операция также завершается собственной ошибкой превышения ожидания.

**Что под пределом не стоит.** Повторное открытие найденных объектов каталога и чтение их свойств на путях синхронизации пользователей и выгрузки работает как раньше, без собственного предела. Отмена операции по разрыву HTTP-запроса пользователем в объём предела не входит.

## 9. MSSQL vs PostgreSQL — различия

### Ограничение: PG не поддерживает полный AD sync

На PostgreSQL часть операций синхронизации работает иначе, чем на MS SQL:

| Область | MSSQL | PG |
|---------|-------|----|
| Нормализация членства (`tc_NormalizeGroupUsersMembership`) | Выполняется (шаг 7) | **Не выполняется** |
| Создание новых групп из AD | Работает | Ограничено |
| Финальное сохранение изменений | Да | **Нет** |

**Последствия отсутствия `tc_NormalizeGroupUsersMembership` на PG:**

- Вложенность групп не нормализуется после синхронизации
- `UserGroups` не получает записи, основанные на рекурсивном обходе `GroupParents`
- `GroupUsersMembership` не ведёт историю
- Создание новых групп из AD на PG не работает

**Обход леса AD поддерживается и на PostgreSQL.** Перечисление доменов леса, раскрытие дерева и выгрузка из дочерних доменов выполняются в том числе в linux-контуре, где работу с каталогом обеспечивает прокси на библиотеке Novell LDAP. Перечисленные выше ограничения относятся к шагам синхронизации, а не к обходу леса.

## 10. Поток данных

Схема показывает, каким таблицам и колонкам базы 1Ф соответствуют объекты и атрибуты Active Directory:

```
Active Directory (LDAP)              1Forma DB
=======================              =========

Users                                Users
  objectSid         ──────────────►    SID
  sAMAccountName    ──────────────►    Nick
  givenName         ──────────────►    FirstName
  sn                ──────────────►    LastName
  mail              ──────────────►    Email
  (mapped attrs)    ──────────────►    (mapped fields)
  (ext attrs)       ──────────────►    UserInfoExtValues

Groups                               Groups
  objectSid         ──────────────►    ADSID
  displayName       ──────────────►    Descr
  (domain)          ──────────────►    Domain

Membership
  group.member      ──────────────►    UserGroupsActual (прямое)
                    ──[SP]────────►    UserGroups (развёрнутое с учётом вложенности)
                    ──[SP]────────►    GroupUsersMembership (история)

Nesting
  group.memberOf    ──────────────►    GroupParents (parent-child)
```

## 11. Диагностика

### SQL-запросы для проверки состояния AD sync

Запросы ниже помогают быстро оценить состояние синхронизации; выполняются в базе данных 1Ф:

```sql
-- Активные профили синхронизации
SELECT sp.Id, ss.Description, ss.ServiceType,
       ads.SyncUserData, ads.SyncGroupData, ads.SyncGroupCreation,
       ads.SyncUserGroupMembership, ads.SyncGroupNesting,
       ads.MaximumADSyncRows
FROM SynchronizationProfiles sp
JOIN ServicesSettings ss ON sp.ServiceId = ss.Id
JOIN SynchronizationProfilesADSettings ads ON ads.SynchronizationProfileId = sp.Id
WHERE sp.IsActive = 1;

-- Пользователи с SID (привязаны к AD)
SELECT COUNT(*) AS TotalLinked FROM Users WHERE SID IS NOT NULL AND LEN(LTRIM(RTRIM(SID))) > 0;

-- Группы с AD sync
SELECT GroupID, Descr, ADSID, Domain, EnableADSync
FROM Groups WHERE EnableADSync = 1;

-- Маски для создания групп
SELECT m.Wildcard, ads.SynchronizationProfileId
FROM ADGroupSyncMasks m
JOIN SynchronizationProfilesADSettings ads ON m.SynchronizationProfileADSettingsId = ads.Id;

-- Маппинг свойств
SELECT OfProperty, AdProperty, Entity
FROM ADPropertyMapping WHERE SynchronizationProfileId = @profileId;

-- Последний запуск ADSyncJob (Quartz)
SELECT TOP 1 * FROM QRTZ_FIRED_TRIGGERS WHERE JOB_NAME LIKE '%ADSync%' ORDER BY FIRED_TIME DESC;
```

### Типовые проблемы

Наиболее частые проблемы синхронизации и способы их диагностики:

| Симптом | Причина | Диагностика |
|---------|---------|-------------|
| Группы не создаются из AD | PG: SP отсутствует; MSSQL: маска не задана или `SyncGroupCreation = false` | Проверить флаги + маски + `UsePostgreSQLDatabase` |
| Пользователи не синхронизируются | `SyncExistingUsers = false` (главный выключатель) | `SELECT SyncExistingUsers FROM SynchronizationProfilesADSettings` |
| Тихий сбой (нет ошибок и нет изменений) | `MaximumADSyncRows` = 10 (по умолчанию), строк больше лимита → откат | Проверить значение лимита, увеличить |
| Дубликаты SID → ошибка | В `Users` две записи с одинаковым SID | `SELECT SID, COUNT(*) FROM Users GROUP BY SID HAVING COUNT(*) > 1` |
| Сотрудник заводится повторно, с дублем | Карточку сохранили, когда провайдер ActiveDirectory был деактивирован: поле «Провайдеры» у доменного пользователя пустое, и сохранение очистило `Users.SID` вместе с `Users.DomainController` — запись выпала из привязанных, и синхронизация создала её заново | Сверить `SID` у записи; сделать провайдер активным, выбрать его в карточке и сохранить SID — см. [auth/admin.md](https://help.1forma.ru/domains/auth/admin.md#1-провайдеры-аутентификации); затем найти и свести уже созданный дубль |
| Ошибка обращения к глобальному каталогу AD | Некорректная настройка `LDAP_AdGlobalCatalogHosts` | Проверить список GC-хостов; если не помогает — обратиться в поддержку 1Ф |
| Фильтр OU не работает | Известный баг: при включённом фильтре OU пользователи не создаются | Обратиться в поддержку 1Ф |
| Дерево подразделений не раскрывается, ответ `504 Gateway Time-out` | Операция с каталогом не уложилась в лимит прокси: до появления предела ожидания запрос уходил в каталог и не завершался, а записи в журнале не оставалось | На сборках с пределом ожидания (см. [8.1](#81-предел-ожидания-операций-с-каталогом)) вместо 504 приходит своя ошибка с хостом, базой, областью и длительностью, а в журнал — запись уровня предупреждение; найти её поиском по тексту «Превышен предел ожидания операции с каталогом». Если 504 продолжается — обновить площадку |
| В журнале нет ни записи об операции, ни ошибки при раскрытии дерева | Площадка на сборке без предела ожидания: приложение не успевает ни ответить, ни записать ошибку | Сверить версию сборки; при подтверждении передать площадку на доработку |
| После смены ФИО в каталоге подписи пользователя остались прежними | Не пересобраны отображаемые имена (`dbo.UserNameModes`) | `select UserID, LanguageID, UserNameMode, DisplayName from dbo.UserNameModes where UserID = @userId` — после синхронизации должны быть новые ФИО по каждому языку пользователя |

## 12. Известные ограничения и edge cases

При планировании и настройке синхронизации учитывайте следующие ограничения:

1. **PG: нет `tc_NormalizeGroupUsersMembership`** — вложенность групп не нормализуется автоматически. Создание новых групп из AD на PG не работает.
2. **OS Mutex `Global\ADSyncJob`** — защита от параллельного ручного и автоматического запуска.
3. **Транзакция 300 минут** — при большом количестве пользователей может не хватить.
4. **MaximumADSyncRows по умолчанию = 10** — очень мало для реальных площадок, часто вызывает тихий откат без ошибок.
5. **SID-дубликаты** — при дубликатах SID в БД синхронизация завершается ошибкой.
6. **Динамические группы AD не поддерживаются.**
7. **Символы `#` и `&` в логинах/паролях** — запрещены, вызывают ошибку при синхронизации.
8. **`ActiveDirectoryAuthenticationMode`** — при одноимённых учётках в лесе AD нужен `PrincipalContext`, иначе ошибки аутентификации.
9. **Инвалидация токенов при смене пароля** — при смене пароля в AD фоновый процесс проверки провайдеров аутентификации (каждые 15 минут) автоматически инвалидирует ранее выданные токены.

10. **`ObjectDisposedException` при чтении отдельных AD-атрибутов на Windows** — устранено на уровне платформы; см. [support-guide-ad-sso.md](https://help.1forma.ru/domains/auth/support-guide-ad-sso.md), п. 1.5.

11. **1F Certificate Edition: синхронизация с каталогом изъята** — нет задачи расписания, нет четырёх административных маршрутов (404), нет автосоздания пользователя и синхронизации членства при входе; профили и записи расписания остаются в базе. Подробности — раздел 13.

12. **Предел ожидания не отменяет отмену по HTTP** — если пользователь закрыл страницу или разорвал запрос, операция с каталогом не прерывается: она доходит до своего предела и завершается собственной ошибкой. Перевод дерева и выгрузки на прерываемый по HTTP запрос — отдельная работа.

13. **Предел ожидания действует только на интерактивные операции** — раскрытие дерева подразделений и обход доменов леса. Политика выбирается по операции, а не по способу вызова: массовая операция, выполняемая через примитив чтения узла дерева, предела не получает. Неполнота выдачи помечается в результате: серверный предел выборки каталога, отказ страницы домена леса и нативный таймаут считаются неполным результатом — полный список в этом случае не выдаётся, а при полном исчерпании операция завершается ошибкой. Массовые поиски — синхронизация пользователей AD, выгрузка пользователей, проверка SID при сохранении карточки — под предел не попадают: их результат проверяется на полноту пагинации, поэтому неполный ответ каталога не приводит к частичной загрузке или ошибочной перезаписи данных.

## 13. Синхронизация с каталогом в 1F Certificate Edition

В сертифицированной сборке (1F Certificate Edition, профиль решения `UniForm.Barebone`, символ условной компиляции `BAREBONE`) синхронизация с каталогом из состава сборки изъята. Механизм — условная компиляция: правки в `core` не меняют поведение полной сборки `UniForm/UniForm.slnx` ни на одну строку.

### 13.1 Что отсутствует в сборке

| Точка входа | Где | Поведение в профиле |
|---|---|---|
| Задача расписания `ADSyncJob` | `TCClassLib/QuartzJobs/Jobs/ADSyncJob.cs`, регистрация в `SchedulerFactory.JobsDictionary` (поле `BuiltinJobsDictionary`) | Класс не компилируется, регистрация обёрнута `#if !BAREBONE`. Ежедневного запуска в 20:00 нет; соседние задачи расписания на месте |
| `api/admin/ldap/*` | `LdapController` (`search-users`, `search-groups`, `providers`), файл исключён из `Uniform.Admin.Api.csproj` | Маршрутов нет, ответ 404 |
| `api/sync-ad/*` | `SyncAdController` (13 действий), папка исключена из `UniForm.Api.csproj` | Маршрутов нет, ответ 404 |
| `POST api/admin/users/{id}/sync-with-ad` | `UserManageController.SyncWithAd` | Метод под `#if !BAREBONE`, маршрута нет, ответ 404 |
| `GET api/admin/groups/domains` | `GroupsController.GetDomains` | Метод под `#if !BAREBONE`, маршрута нет, ответ 404 |
| Синхронизация членства при входе | `UserActiveDirectoryLookupService` (файл исключён из `TCClassLib.csproj`), ветка отказа и метод `CreateUser` в `WinClaimsTransformation` | Файл не компилируется, обращения к каталогу при входе не происходит |
| Класс параметра смарт-действия синхронизации | `ActionParameterSyncUserWithAD` + регион `SynchronizationProfileADDto` в `PacksLogic` | Исключены, в списке доступных действий их нет |

Ядро синхронизации в этот профиль не компилируется: из `TCClassLib` исключены `DirectoryInterop/**` и `Synchronization/ActiveDirectory/**`, из `Valhalla.ExternalServices` — `ActiveDirectory/**`, `OpenLDAP/**`, `DirectorySearch/LDAP/**` и `DirectorySearch/ActiveDirectory/**` вместе с конфигураторами провайдеров. Условная ссылка на пакеты каталога (`System.DirectoryServices*`, `Novell.Directory.Ldap.NETStandard`, `AntiLdapInjection`) в профиле снята. Обращения к снятым типам вне этого кода отвязаны условной компиляцией: сервис запроса статуса пользователя в каталоге `UserActiveDirectoryStatusRequestService` не компилируется целиком, а потребители — управление пользователями и группами, поставщик свойств пользователя, обработчик смарт-действий синхронизации — работают без обращений к каталогу.

Прочие маршруты тех же контроллеров работают как прежде. Отдельных ответов и специальных кодов не заведено: 404 — общее правило каркаса.

### 13.2 Вход через Windows-идентичность

Пользователь разрешается только по локальной базе 1Формы: по SID (`UserClaimsMapperBySid`) либо по UPN при соответствующей настройке Negotiate. Поиск пользователя в каталоге, автосоздание пользователя при первом входе и синхронизация его членства в группах в профиле отсутствуют: если пользователя с таким SID в базе 1Ф нет, вход не проходит (`UserNotFoundException`).

### 13.3 Что остаётся

- Таблицы профилей и настроек синхронизации (`SynchronizationProfiles`, `SynchronizationProfilesADSettings`, `ADPropertyMapping`, `LDAPServicesCredentials` / `OpenLDAPServicesCredentials`), записи расписания, поля `Users.SID` / `Groups.ADSID` и ресурсы-строки — без изменений. Профиль, настроенный до перехода на сертифицированную сборку, остаётся в базе, но больше не читается: ни задача, ни маршруты к нему не обращаются.
- Клиентский (фронтовый) раздел администрирования синхронизации по решению владельца не менялся: он остаётся в интерфейсе и ведёт в снятые маршруты (404). Сокрытие AD-специфичных действий на клиенте — отдельная работа.
- Удаление участника группы в профиле проходит только по общему пути `AdminGroups`, где стоит проверка последнего администратора: попытка удалить единственного администратора отклоняется (`Language.errCantDeleteLastUser`). Синхронизационного кода, менявшего членство в обход этой проверки, в сборке нет.
- Состав сборки проверяют сторожа `DirectorySyncBareboneAbsenceTests` (в профиле проверяет отсутствие изъятых типов ядра каталога и задачи `ADSyncJob` в `BuiltinJobsDictionary`) и `BareboneTestSourcesIsolationTests` (проверяет, что тесты, ссылающиеся на изъятые имена, исключены из компиляции). Оба лежат в `Tests/TCClassLib.Tests.Unit.Core/Barebone/`.
