Интеграция 1С и Active Directory на сервере

Публикация № 1171021

Методология - DevOps - Скрипты автоматизации

Интеграция Active Directory сервере Сервер НаСервере Пользователи EMAIL MAIL почта телефон

Получить информацию Active Directory на управляемых формах со стороны сервера, под любым пользователем домена.

Предлагаю вам рассмотреть возможность получения информации из Active Directory на стороне сервера. 

&НаСервере
Функция ПолучитьИнформациюActiveDirectoryНаСервере(пИмяДомена, пПользователь, пПароль, пУсловияОтбора)
	Перем Пользователь,Пароль,Домен,Стр,Контроллер,Отбор,Подразделение,Группа;
	
	//ADsPath,Name,Mail,telephoneNumber,displayName,sAMAccountName,department
	СтрКолонок = Новый Структура("ADsPath,sn,givenName,Name,Mail,telephoneNumber,displayName,sAMAccountName,department,userPrincipalName");
    ТаблицаРезультат = Новый ТаблицаЗначений;
	Для Каждого СтрКол Из СтрКолонок Цикл 
		ТаблицаРезультат.Колонки.Добавить(СтрКол.Ключ, Новый ОписаниеТипов("Строка"));
	КонецЦикла;
	ТаблицаРезультат.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
	ТаблицаРезультат.Колонки.Добавить("АутентификацияОС", Новый ОписаниеТипов("Строка"));
	ТаблицаРезультат.Колонки.Добавить("Фамилия", Новый ОписаниеТипов("Строка"));
	ТаблицаРезультат.Колонки.Добавить("Имя", Новый ОписаниеТипов("Строка"));
	ТаблицаРезультат.Колонки.Добавить("Отчество", Новый ОписаниеТипов("Строка"));
	
	ТаблицаРезультат.Колонки.Добавить("Служебный", Новый ОписаниеТипов("Булево"));
	ТаблицаРезультат.Колонки.Добавить("ДоменWINS", Новый ОписаниеТипов("Строка"));
	ТаблицаРезультат.Колонки.Добавить("ДоменDNS", Новый ОписаниеТипов("Строка"));
	
	
	AD_Domain_DC = пИмяДомена;   //Имя домена - Перед (Windows 2000), Имя WINS
	//Домен="....................";    //Каноническое имя домена, Имя DNS
	
	Пользователь = пПользователь;
	Пароль		 = пПароль;
	
    АДСИ = Новый COMОбъект("ADSystemInfo");
    Домен = АДСИ.DomainDNSName;		   //Каноническое имя домена, Имя DNS
	
	//Но на самом деле хватит и обычного пользователя домена
    //Пользователь="администратор@" + Домен;
    //Пароль="СуперпарольАдминистратора";

    ДСО = ПолучитьCOMОбъект("GC:");
    Корень = ДСО.OpenDSObject("GC://RootDSE", ""+Пользователь+"@"+Домен, Пароль,1);
	
	НашДомен = Корень.Get("defaultNamingContext");
	
	ЗаписьАДО = Новый COMОбъект("ADODB.recordset");
	СоединениеАДО = Новый COMОбъект("ADODB.Connection");
	СоединениеАДО.Provider = "ADsDSOObject"; 
	
	ADS_SECURE_AUTHENTICATION = 1;     // Позволяет читать данные, когда не разрешен анонимный доступ 
	
	СоединениеАДО.Properties("User ID"  ).Value = AD_Domain_DC+"\"+Пользователь;
	СоединениеАДО.Properties("Password" ).Value = Пароль;
	СоединениеАДО.Properties("ADSI Flag").Value = ADS_SECURE_AUTHENTICATION;
	
	СоединениеАДО.Open("ADs Provider");
	
	Если пУсловияОтбора = 1 Тогда
		//Все пользователи, кроме отключенных
		ТектсУсловияОтбора = "(!useraccountcontrol:1.2.840.113556.1.4.803:=2)";
		
	ИначеЕсли пУсловияОтбора = 2 Тогда
		//Только отключенные пользователи
		ТектсУсловияОтбора = "(useraccountcontrol:1.2.840.113556.1.4.803:=2)";
		
	Иначе 
		//Все пользователи...
		ТектсУсловияОтбора = "";
	КонецЕсли;
	
	ПоляЗапроса = "ADsPath,sn,givenName,Name,Mail,telephoneNumber,displayName,sAMAccountName,department,userPrincipalName";
	ЗаписьАДО.Open("<GC://" + СокрЛП(НашДомен) + ">;(&(objectCategory=person)(objectClass=user)"+ТектсУсловияОтбора+"); "+ПоляЗапроса+" ;subtree", СоединениеАДО, 0, 1);
	
	
	//ADsPath,Name,Mail,telephoneNumber,displayName,sAMAccountName,department
	//Сообщить("ИмяПользователя " + СокрЛП(ВыборкаАДО.Fields("displayName").Value));
	//Сообщить("ИмяАккаунта "     + СокрЛП(ВыборкаАДО.Fields("sAMAccountName").Value));
	//Сообщить("Подразделение "   + СокрЛП(ВыборкаАДО.Fields("department").Value));
	//Сообщить("Почта "           + СокрЛП(ВыборкаАДО.Fields("mail").Value));
	//Сообщить("Телефон "         + СокрЛП(ВыборкаАДО.Fields("telephoneNumber").Value));
	
	Если ЗаписьАДО.RecordCount > 0 Тогда
		Пока Не ЗаписьАДО.EOF Цикл
			
			Для Каждого СтрКол Из СтрКолонок Цикл 
				СтрКолонок[СтрКол.Ключ] = ЗаписьАДО.Fields(СтрКол.Ключ).Value;
			КонецЦикла;
			
			НоваяСтрока = ТаблицаРезультат.Добавить();
			ЗаполнитьЗначенияСвойств(НоваяСтрока,СтрКолонок);
			Если НЕ ПустаяСтрока(НоваяСтрока.userPrincipalName) Тогда 
				НоваяСтрока.АутентификацияОС = "\\"+AD_Domain_DC+"\"+НоваяСтрока.sAMAccountName;
			КонецЕсли;
			
			//Наименование
			НоваяСтрока.Наименование = СокрЛП(НоваяСтрока.displayName);
			Если ПустаяСтрока(НоваяСтрока.Наименование) Тогда 
				НоваяСтрока.Наименование = СокрЛП(НоваяСтрока.Name);
			КонецЕсли;
			
			//Отчество
			НоваяСтрока.Фамилия	 = СокрЛП(НоваяСтрока.sn);
			НоваяСтрока.Имя		 = СокрЛП(НоваяСтрока.givenName);
			НовоеИмя = "";
			НоваяСтрока.Отчество = ПолучитьОтчествоИзАД(НоваяСтрока.sn, НоваяСтрока.givenName, НоваяСтрока.Name, НовоеИмя);
			Если НЕ ПустаяСтрока(НовоеИмя) Тогда
				НоваяСтрока.Имя	 = НовоеИмя;
			КонецЕсли;
			
			НоваяСтрока.Служебный = ПустаяСтрока(НоваяСтрока.userPrincipalName);
			НоваяСтрока.ДоменWINS = AD_Domain_DC;
			НоваяСтрока.ДоменDNS  = Домен;
	
			Попытка
				ЗаписьАДО.MoveNext();
			Исключение
				Прервать;
			КонецПопытки;
			
		КонецЦикла;
	КонецЕсли;
	
	ЗаписьАДО.Close();
	ЗаписьАДО = Неопределено;
	СоединениеАДО.Close();
	СоединениеАДО = Неопределено;
	
	
	Возврат ТаблицаРезультат; 
КонецФункции

 

 
Доп. функции использованные в функции ПолучитьИнформациюActiveDirectoryНаСервере()

 

&НаСервере
Функция УдалитьДвойныеПробелыИзСтроки(ФИО)
	
	РезФун = СокрЛП(ФИО);
	РезФун = СтрЗаменить(РезФун, "  ", " ");
	Пока Найти(РезФун, "  ") > 0 Цикл 
		РезФун = СтрЗаменить(РезФун, "  ", " ");
	КонецЦикла;
	
	Возврат РезФун;
КонецФункции


&НаСервере
Функция ПолучитьОтчествоИзАД(Знач Фамилия, Знач Имя, Знач ФИО, НовоеИмя)
	
	Если ПустаяСтрока(Фамилия) или ПустаяСтрока(Имя) или ПустаяСтрока(ФИО) Тогда 
		Возврат "";
	КонецЕсли;
	
	Фамилия = СокрЛП(Фамилия);
	Имя = СокрЛП(Имя);
	ФИО = СокрЛП(ФИО);
	
	//Избавимся от лишних пробелов ФИО
	ФИО = УдалитьДвойныеПробелыИзСтроки(ФИО);
	Имя = УдалитьДвойныеПробелыИзСтроки(Имя);
	
	МассивСтрок = СтрРазделить(ФИО," ",Истина);
	Если МассивСтрок.Количество() <> 3 Тогда Возврат ""; КонецЕсли;
	
	МассивСтрокИмя = СтрРазделить(Имя," ",Истина);
	
	//Убрать Фамилию
	Если Найти(ФИО+" ", Фамилия+" ") = 0 Тогда Возврат ""; КонецЕсли;
	ФИО = СокрЛП(СтрЗаменить(ФИО+" ", Фамилия+" ", ""));
	
	//Убрать Имя
	Если МассивСтрокИмя.Количество() = 2 Тогда
		НовоеИмя = СокрЛП(МассивСтрокИмя[0]);
		НовоеОтчество = СокрЛП(МассивСтрокИмя[1]);
		
		//Убрать Имя
		Если Найти(ФИО+" ", НовоеИмя+" ") = 0 Тогда 
			НовоеИмя = "";
			Возврат ""; 
		КонецЕсли;
		ФИО = СокрЛП(СтрЗаменить(ФИО+" ", Фамилия+" ", ""));
		
		//Найти Отчество
		Если Найти(ФИО+" ", НовоеОтчество+" ") = 0 Тогда 
			НовоеИмя = "";
			Возврат ""; 
		КонецЕсли;
		
		ФИО = НовоеОтчество;
		
	Иначе 
		Если Найти(ФИО+" ", Имя+" ") = 0 Тогда Возврат ""; КонецЕсли;
		ФИО = СокрЛП(СтрЗаменить(ФИО+" ", Имя+" ", ""));
	КонецЕсли;
	
	Возврат ФИО;
КонецФункции

 

 

Примеры запросов Active Directory

  1. Поиск административных учётных записей по атрибуту adminCount. Если пользователь является членом защищенной группы (например, Domain Admins, Enterprise Admins и т.п.), ему назначаются ACL, установленные в объекте AdminSDHolder в AD, а атрибуту adminCount пользователя присваивается значение 1.
    (objectClass=user)(objectCategory=Person)(adminCount=1)

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

  2. Все объекты, защищённые AdminSDHolder:
    (adminCount=1)
  3. Поиск всех отключенных учётных записей компьютеров:
    (&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=2))
  4. Все пользователи:
    (&(objectCategory=user)(objectClass=user)(userPrincipalName=*))

    Или более эффективный вариант:

    (sAMAccountType=805306368)
  5. Все пользователи, кроме отключенных:
    (objectCategory=person)(objectClass=user)(!useraccountcontrol:1.2.840.113556.1.4.803:=2)
  6. Пользователи, которые должны сменить пароль:
    (objectCategory=person)(objectClass=user)(pwdLastSet=0)(!useraccountcontrol:1.2.840.113556.1.4.803:=2)
  7. Пользователи, которые будут заблокированы после ещё одной неудачной попытки ввода пароля. В запросе используется атрибут badPwdCount. Значение нужно указать в соответствии с вашими парольными политиками в домене и учётом гранулированных парольных политик, если используются.
    (objectCategory=user)(badPwdCount>=6)
  8. Пользователи с бессрочным паролем (Password never expires):
    (objectcategory=user)(userAccountControl:1.2.840.113556.1.4.803:=65536)
  9. Все пользователи, которые не меняли свой пароль с указанной даты
    (&(objectCategory=person)(objectClass=user)(pwdLastSet<=131720255990000000))

    PwdLastSet для определённой даты можно получить с помощью PowerShell:

    $date = '28 May 2018 23:59:59' (Get-Date $date).ToFileTimeUTC()

    Подробнее о атрибуте Pwd-Last-Set см. здесь.

  10. Заблокированные пользователи:
    (objectCategory=person)(objectClass=user)(useraccountcontrol:1.2.840.113556.1.4.803:=16)
  11. Пользователи, у которых не заполнено поле должность. По аналогии можно найти пользователей по другим полям, либо по маске *название должности:
    (&(objectCategory=user)(objectClass=user)(!title=*))
  12. Пользователи, созданные за определённый период. Формат дат используется следующий:
    YYYY MM DD HH mm ss.s Z 2017 01 01 00 00 00.0 Z
    (objectCategory=user)(whenCreated>=20170101000000.0Z&<=20171201000000.0Z&)

    objectCategory можно использовать любой, например group, computer, contact и др.

  13. Пользователи без фотографии (атрибут thumbnailPhoto):
    (objectCategory=user)(objectClass=user)(userPrincipalName=*)(!thumbnailPhoto=*)
  14. Все пользователи с Email адресом:
    (objectcategory=person)(mail=*)
  15. Все пользователи без Email адреса:
    (objectcategory=person)(!mail=*)
  16. Все пользователи у которых переопределена квота на почтовый ящик Exchange:
    (&objectCategory=user)(mDBUseDefaults=FALSE)
  17. Все пользователи, скрытые из адресной книги Exchange:
    (objectCategory=person)(objectClass=user)(msExchHideFromAddressLists=TRUE)
  18. Все компьютеры:
    (objectCategory=computer)
  19. Все компьютеры Windows 10:
    (objectCategory=computer)(operatingSystem=Windows 10*)
  20. Все компьютеры Windows Server 2016:
    (objectCategory=computer)(operatingSystem=Windows Server 2016*)
  21. Windows Server 2016 Standard:
    (objectCategory=computer)(operatingSystem=Windows Server 2016 Standard)
  22. Windows Server 2016 Datacenter:
    (objectCategory=computer)(operatingSystem=Windows Server 2016 Datacenter)
  23. Все SQL серверы, с любой ОС, у которых зарегистрирован servicePrincipalName:
    (objectCategory=computer)(servicePrincipalName=MSSQLSvc*)(operatingSystem=Windows Server*)
  24. Все Exchange серверы:
    (objectCategory=computer)(servicePrincipalName=exchangeMDB*)(operatingSystem=Windows Server*)
  25. Все контакты:
    (objectClass=contact)
  26. Поиск SMTP адреса по атрибуту proxyAddress. Позволяет найти любые объекты с почтовым адресом, включая общие папки Exchange:
    (&proxyAddresses=smtp:*@domain.com)
  27. Все группы:
    (objectCategory=group)
  28. Пустые группы, не содержащие ни одного члена:
    (objectCategory=group)(!member=*)
  29. Все организационные подразделения:
    (objectCategory=organizationalUnit)
  30. Все контейнеры:
    (objectCategory=container)
  31. Все цветные принтеры на сервере печати:
    (uncName=*Servername*)(objectCategory=printQueue)(printColor=TRUE)

    Servername — имя сервера печати, на котором установлен и опубликован в Active Directory принтер.

  32. Все доверительные отношения:
    (objectClass=trustedDomain)

 

Пример получения E_MAIL на стороне Клиента.

В качестве входного параметра должна быть указана строка вида \\Домен\Пользователь_домена

&НаКлиенте
Функция ПолучитьАдресИзADO(Знач ПользовательОС) Экспорт 
	
	Email = "";
	
	Если ПустаяСтрока(ПользовательОС) Тогда Возврат Email; КонецЕсли;
	
	Попытка
		
		RootDSE = ПолучитьCOMОбъект("LDAP://RootDSE");
		НашДомен = RootDSE.Get("rootDomainNamingContext");
	
		ЗаписьАДО = Новый COMОбъект("ADODB.recordset");
		СоединениеАДО = Новый COMОбъект("ADODB.Connection");
		СоединениеАДО.Provider = "ADsDSOObject"; 
		СоединениеАДО.Open("ADs Provider");
		
		СтрокаПользователя = СокрЛП(ПользовательОС);
		КоличествоСимволов = СтрЧислоВхождений(СтрокаПользователя,"\");
		Пока КоличествоСимволов > 0 Цикл
			Позиция = Найти(СтрокаПользователя,"\");
			СтрокаПользователя = Сред(СтрокаПользователя, Позиция + 1);
			КоличествоСимволов = СтрЧислоВхождений(СтрокаПользователя,"\");
		КонецЦикла;
		
		ЗаписьАДО.Open("<LDAP://" + СокрЛП(НашДомен) + ">;(&(objectCategory=person)(objectClass=user)(sAMAccountName=" + СтрокаПользователя + "));ADsPath, Mail;subtree", СоединениеАДО, 0, 1);
		
		Если ЗаписьАДО.RecordCount > 0 Тогда
			Пока Не ЗаписьАДО.EOF Цикл
				
				Email = ЗаписьАДО.Fields("Mail").Value;
				
				Попытка
					ЗаписьАДО.MoveNext();
				Исключение
					Прервать;
				КонецПопытки;
				
			КонецЦикла;
		КонецЕсли;
		
		ЗаписьАДО.Close();
		ЗаписьАДО = Неопределено;
		СоединениеАДО.Close();
		СоединениеАДО = Неопределено;
		
		Возврат СокрЛП(Email);
		
	Исключение
		Сообщить(ОписаниеОшибки());
		Возврат Email;
	КонецПопытки;
	
КонецФункции // ПолучитьАдресИзADO()

 

Основные атрибуты Active Directory

Таблица основных пользовательских атрибутов Active Directory

Attribute \ Атрибут Англоязычное название Русскоязычное название Value \ Значение
OU (Organizational Unit) \ Подразделение
distinguishedName  Distinguished Name  Отличительное (уникальное) имя OU=Компания,DC=domain,DC=com
name     Компания
Group \ Группа
distinguishedName  Distinguished Name Отличительное (уникальное) имя CN=Группа,OU=Компания,DC=domain,DC=com
name     Группа
member Members Члены группы (какие пользователи входят в данную группу) CN=Сергей Петрович Иванов,OU=Компания,DC=domain,DC=com
User \ Пользователь
DN Distinguished Name Отличительное (уникальное) имя CN=Сергей Петрович Иванов,OU=Компания,DC=domain,DC=com
DC Domain Component Компонент(класс) доменного имени. DC=domain,DC=com
OU Organizational Unit Подразделение Компания
CN Common Name Общее имя Сергей Петрович Иванов
givenName First name Имя Сергей Петрович
name Full name Полное имя Сергей Петрович Иванов
sn (SurName) Last name Фамилия Иванов
displayName Display Name Выводимое имя Сергей Петрович Иванов
mail E-mail Электронная почта mail@domain.com
sAMAccountName User logon name (pre-Windows 2000) Имя входа пользователя (пред-Windows 2000) IvanovSP
userPrincipalName User logon name Имя входа пользователя IvanovSP@domain.com
memberOf Member Of Член групп (в какую группу входит данный пользователь) CN=Группа,OU=Компания,DC=domain,DC=com

 

Атрибут userAccountControl

Иногда надо понять включена или отключена учетная запись в AD. Или что еще с ней вообще происходит. За это отвечает атрибут userAccountControl, который является суммой нескольких свойств атрибутов. При этом, значение 512 является значением по умолчанию при всех снятых флагах на вкладке «Учетная запись» и каждый дополнительный параметр прибавляется к нему. Например, значения атрибута userAccountControl для наиболее распространенных случаев:
512 — Включена (Enabled)
514 (512+2) — Отключена (Disabled)
66048 (512+65536) — Включена, срок действия пароля не ограничен (Enabled, password never expires)
66050 (512+65536+2) — Отключена, срок действия пароля не ограничен (Disabled, password never expires)

Список основных значений атрибутов userAccountControl:

HEX DEC Описание
0x00000002 2 Учетная запись отключена
0x00000010 16 Учетная запись заблокирована
0x00000020 32 Пароль не требуется
0x00000040 64 Запретить смену пароля пользователем
0x00000080 128 Хранить пароль, используя обратимое шифрование
0x00000200 512 Учетная запись по умолчанию. Представляет собой типичного пользователя
0x00010000 65536 Срок действия пароля не ограничен
0x00040000 262144 Для интерактивного входа в сеть нужна смарт-карта
0x00400000 4194304 Без предварительной проверки подлинности Kerberos
0x00800000 8388608 Пароль пользователя истек

 

 

Использовал следующий материал для написания функции:

 

Приложил обработку, но в основном она состоит из одной функции. Тестирование произведено на версии платформы 8.3.14.1976. Но к платформе код не имеет отношения. Версия конфигурации тоже неважна, работает на любой.

 

 

Скачать файлы

Наименование Файл Версия Размер
Интеграция 1С и Active Directory на сервере:
.epf 15,67Kb
21.12.19
11
.epf 1.1.3 15,67Kb 11 Скачать

Специальные предложения

Комментарии
Избранное Подписка Сортировка: Древо
1. YPermitin 7231 22.12.19 16:16 Сейчас в теме
2. DrZombi 110 22.12.19 16:40 Сейчас в теме
Для автоматизации Документооборота, самое то ;)
user771073; acanta; user774630; vovan_victory; jONES1979; YPermitin; +6 Ответить
3. Bonov 23.12.19 12:07 Сейчас в теме
Вот еще бы для Linux-сервера придумали ;)
4. DrZombi 110 23.12.19 12:38 Сейчас в теме
(3) Поготь, Обяжут, придумаем ;)
5. ander_ 24.12.19 10:13 Сейчас в теме
6. sergathome 26.12.19 16:29 Сейчас в теме
Столкнулся со следующей проблемой - на сервере сервис исполняется от имени локального пользователя. Для доступа к АД используется соединение под доменным. Читается всё хорошо и прекрасно... Проблема возникает если нужно внести в АД изменения, допустим поменять телефон пользователя. Суть проблемы - в 1С отсутствует возможность получить объект на запись под пользователем, отличным от текущего, просто синтаксиса даже такого нет (или я не нашёл) - везде предлагается использовать ПолучитьСОМОбъект, при исполнении которого происходит собственное неявное соединение с АД с потерей контекста любого созданного ранее соединения. Печаль.
7. DrZombi 110 26.12.19 21:51 Сейчас в теме
(6)
Проблема возникает если нужно внести в АД изменения


А вот тут вам надо уже "СУПЕР АДМИНСКИЕ" права.
9. sergathome 27.12.19 09:12 Сейчас в теме
(7) Неа. Ничего подобного, никаких чудес и суперправ. Просто другая модель нужна. И она есть, как выяснилось. Только документацию хрен найдёшь.
Root_AD = ПолучитьCOMОбъект("LDAP:");
ОбъектАД = Root_AD.OpenDSObject(ПутьОбъекта,Т.Логин,Т.Пароль,100);
Дмитрий74Чел; +1 Ответить
8. DrZombi 110 26.12.19 21:52 Сейчас в теме
Мне нужна была такая модель только для получения почты и телефона, для генерации и актуализации адресной книги в 1С.
10. Дмитрий74Чел 182 21.01.20 15:13 Сейчас в теме
Версия конфигурации тоже неважна, работает на любой.

Есть код для регистрации внешней обработки в справочнике обработок и обращение к "ДополнительныеОтчетыИОбработкиКлиентСервер". На "голой" самописке выдает ошибку.
11. Дмитрий74Чел 182 21.01.20 15:23 Сейчас в теме
У нас похоже огромный список. Обработка закончилась на 999 записях, в коде нашел попытка-исключение, воткнул сообщить - получил:
{ВнешняяОбработка.ActiveDirectoryНаСервере.Форма.Форма.Форма(185)}: Ошибка при вызове метода контекста (MoveNext): Произошла исключительная ситуация (Provider): Для этого запроса превышен предел размера.

Как получить более 999 записей?

Похоже можно циклом вытаскивать по 999 записей, но надо как-то передать параметр "очередная первая строка порции".
12. DrZombi 110 21.01.20 15:48 Сейчас в теме
(11) Разве что только поиграться с фильтрами

4. Все пользователи:
(&(objectCategory=user)(objectClass=user)(userPrincipalName=*))

Организовываешь цикл.
От А до Я
(&(objectCategory=user)(objectClass=user)(userPrincipalName=А*))

подробнее по ссылке: ldap-фильтр
Оставьте свое сообщение

См. также

Подборка решений для взаимодействия со ФГИС «Меркурий» Промо

С 1 июля 2019 года все компании, участвующие в обороте товаров животного происхождения, должны перейти на электронную ветеринарную сертификацию (ЭВС) через ФГИС «Меркурий». Инфостарт предлагает подборку программ, связанных с этим изменением.