Защита в DCOM/COM+ Часть 4

Делегация

До W2k, да и в W2k по умолчанию вызов COM-сервера из другого сервера производится от лица учетной записи первого сервера. Если строится распределенная система, содержащая множество серверов, появляется необходимость изменить это положение. Цепь вызовов может соединять несколько серверов, причем каждый из них должен иметь возможность работать от имени клиента, то есть поток сервера иметь возможность имперсонации клиента. Такую возможность предоставляет уровень имперсонации RPC_C_IMP_LEVEL_DELEGATE. Делегация – наиболее мощный уровень имперсонации. Microsoft справедливо боится этого уровня, поскольку непродуманное использование делегации может пробить обширную брешь в защите. Однако препоны, чинимые Microsoft, сильно напоминают паранойю. Включить этот уровень имперсонации – задача не для слабых духом, поэтому имеет смысл уделить ему некоторое внимание.

Аутентификация по протоколу Kerberos создает токен этого уровня при следующих условиях:

Учетная запись, которую вы пытаетесь делегировать, не должна быть помечена в Active Directory как "sensitive and cannot be delegated" и должна быть помечена как "Trusted for delegation".

Учетная запись, под которой работает серверный процесс, должна быть помечена в Active Directory как "Trusted for delegation".

Как этого добиться? Начнем с учетной записи клиента. Сначала запустите на контроллере домена snap-in Active Directory Users and Computers. В свойствах учетной записи на закладке Account включите опцию Trusted for delegation, а опцию Account is sensitive cannot be delegated, наоборот, выключите.

Теперь перейдем к серверному процессу. Если он исполняется под системной учетной записью, принципалом является компьютер, на котором работает этот процесс. Поэтому настройки нужно производить для учетной записи компьютера. Иначе нужно настраивать ту учетную запись, под которой был запущен сервер. Делается это так же, как и для клиента.

Все компьютеры в цепочке вызовов должны работать под W2k или более поздней версией ОС в домене W2k.

Уровень делегации должен быть назначен для proxy, через которую производится вызов либо напрямую, через CoSetProxyBlanket, либо выбираться через алгоритм Security Blanket Negotiation. Во втором случае клиент не должен явно задавать уровень имперсонации, а на сервере должен быть задан именно уровень Delegate.

Делегация доступна только при использовании протокола Kerberos. Как вы помните, в пределах одного компьютера в W2k всегда используется NTLMSSP. Но это не препятствует использованию уровня имперсонации RPC_C_IMP_LEVEL_DELEGATE. Напротив, использовать этот уровень с NTLMSSP даже проще, поскольку не нужна настройка учетных записей. SChannel не поддерживает делегации вообще.

Cloaking

В определении поведения имперсонации участвуют два ингредиента: полномочия, предоставляемые клиентом серверу через уровень имперсонации, и способность сервера скрывать свою идентичность при действиях от лица клиента. Эта способность называется маскировкой (cloaking). Достаточно подробно маскировка рассмотрена в описаниях CoInitializeSecurity и CoSetProxyBlanket.

Cloaking – это просто новый термин для того, что в named pipes и RPC называлось "управление identity tracking (отслеживание учетной записи клиента, от имени которой производится вызов)".

W2k поддерживает и статическую, и динамическую модель маскировки (непонятно, почему в NT 4 это не поддерживалось, ведь MSRPC давным-давно поддерживает эту модель). Интересно, что в W2k появилась третья опция, которую RPC, в сущности, не поддерживает, причем эта опция используется по умолчанию. Я бы назвал ее "игнорировать токен потока", поскольку именно это она и делает: определяя, от какого лица производить исходящий вызов, COM полностью игнорирует токены потоков. Вместо них всегда используется токен процесса. Рисунок 11 показывает различия между тремя моделями маскировки.

 

Защита в службах Windows и DCOM

Рисунок 11.

COM+

Прежде чем говорить о защите в COM+ нужно, наверное, ответить на вопрос – что же такое COM+. Queued-компоненты, поддержка событий, распределенные транзакции – это, хотя и важные и очень полезные, но все же не главные возможности COM+. Прародитель COM+ – MTS сыграл плохую роль. Название "MTS" четко ассоциировало его с менеджерами транзакций. На самом деле MTS, и в особенности COM+ – это замечательный сервер приложений. AppCenter, новый продукт Microsoft, поднимает возможности COM+ до заоблачных высот.

Так что же нового может предложить COM+ в области защиты? И не приведет ли это к еще большему усложнению и без того сложного механизма защиты? Да, в COM+ появился ряд новшеств, расширяющих возможности DCOM, но, к счастью, это не привело к усложнению жизни программистов. Напротив, применение защиты в COM+ упростилось! Да-да, упростилось, несмотря на усложнение. :)

Упростилось оно за счет трех факторов. Во-первых, появилось настоящее средство администрирования. О нем уже говорилось выше – это snap-in Component Services. Во-вторых, многие проблемы, которые раньше можно было решить, только написав море сложного кода, теперь можно решать, задавая атрибут. Атрибуты – это новшество, появившееся в MTS и доведенное до совершенства в COM+. Самое приятное, что их можно задавать как программно, так и декларативно – через средства администрирования. В-третьих, появилась так называемая ролевая безопасность – концепция, похожая на группы в NT, но с несколько иной идеологической подоплекой и преимущественно ориентированная на декларативный стиль работы.

С точки зрения расширения возможностей, прежде всего нужно обратить внимание на то, что в COM+ можно декларативно защищать не только все приложение целиком, но и отдельные его части. В MTS была реализована защита на уровне отдельных COM-компонентов, COM+ снизил планку до интерфейса и даже до отдельного метода. Концепция ролей позволила сделать этот процесс простым и понятным. Вы просто назначаете пользователю некоторые роли, а потом указываете, каким ролям можно вызывать тот или иной метод (получать доступ к интерфейсу, объекту).

Еще одной новостью стало то, что COM+ предоставляет дополнительный сервис через контекст объекта. В этот список вошел и сервис расширенного управления безопасностью.

Ну и, наконец, в COM+ введена замечательная система администрирования. Теперь администратору не нужно бегать от одной машины к другой, копировать нужные файлы и задавать настройки для каждого COM-приложения. COM+ позволяет создавать инсталляторы так называемых COM+-proxy (не путайте их с proxy интерфейсов). А возможность получить программный доступ к настройкам COM+-приложения и клиентских компьютеров дает небывалую гибкость. При этом все администрирование (в том числе установка серверов и клиентских COM+-proxy) может осуществляться удаленно (централизовано).

Ролевая безопасность

Сначала разберемся, как декларативно задать безопасность, и что из этого получится. Откройте snap-in Component Services, найдите в нем приложение ComSec и раскройте его иерархию. У вас должно получиться нечто похожее на рис. 12.

Защита в службах Windows и DCOM

Теперь необходимо настроить защиту приложения так, чтобы COM+ смог полностью показать свои возможности. Для этого откройте свойства приложения и перейдите на закладку Security (см. рис. 13).

Защита в службах Windows и DCOM

Рисунок 13. Страница свойств настройки защиты

Security level позволяет указать, будут проверяться права при доступе ко всему приложению в целом (Perform access checks only at the process level) или при доступе к отдельным объектам и методам (Perform access checks at the process and component level). Первый вариант – это то, что было доступно в обычном DCOM. Второй вариант появился в MTS и был доведен до совершенства в COM+. Переключатель Enforce access checks for this application позволяет включить или выключить поддержку ролевой безопасности.

ПРЕДУПРЕЖДЕНИЕ

Ролевая безопасность может работать, только если выбрана опция Perform access checks at the process and component level. Будьте аккуратны, так как переключатель и данная опция не зависят друг от друга в интерфейсе настройки защиты COM+-приложения.

Выпадающие списки Authentication level for calls и Impersonation level позволяют настроить значения по умолчанию для уровня аутентификации и имперсонации соответственно. Эти значения COM+ подставит в функцию CoInitializeSecurity при запуске COM+-приложения. Их декларативное задание очень актуально для COM+-приложений, так как компоненты, реализуемые в DLL-сервере, не могут самостоятельно вызвать эту функцию.

Задайте для всех настроек такие же значения, как указаны на рисунке 13. Это позволит вам воочию увидеть, как работает ролевая безопасность, а низшие значения уровней имперсонации и аутентификации позволят задать из тестового клиента любые значения для этих настроек.

Теперь настройте роли для объекта, его интерфейса или метода. Для этого выберите сущность, которой нужно назначить роль, откройте её свойства и на закладке Security отметьте необходимые роли. (Как я уже говорил, настроить сами роли и задать ассоциированных с ними пользователей можно в папке Roles COM+-приложения.). Принцип работы ролей таков: если включена ролевая безопасность, то, чтобы получить доступ к методу некоторого объекта, нужно, чтобы хотя бы одна роль, назначенная пользователю (от лица которого производится вызов), была явно включена для объекта, интерфейса или непосредственно метода. По существу, роли очень похожи на группы в NT. Но есть несколько оговорок. Во-первых, роль может быть ассоциирована только с принципалами, т.е. ни группа, ни другая роль не могут быть ассоциированы с ролью. Во-вторых, роль не может содержать запрещений, т.е. вы не можете запретить Иванову вызывать именно этот метод, для этого придется задавать доступ на уровне методов и просто не дать разрешение для ролей, с которыми ассоциирован Иванов. В-третьих, роль следует воспринимать несколько иначе, нежели группу. Надо рассуждать так: роль выделяет некоторые логические типы принципалов. Например, у вашего приложения может быть четыре вида пользователей:

  • Служащий (Employee).
  • Администратор (Admin).
  • Начальник (Manager).
  • Клиент (Customer).

Вы можете задать список принципалов, которым назначена эта роль (например, Интернет-пользователю дать роль Customer, а остальных назначить на роли в соответствии со штатным расписанием), а потом задать роли для отдельных методов. Роль Администратора применяется всюду (ну, возможно, за исключением некоторых методов, возвращающих конфиденциальные данные). Роль Клиента назначается только тем методам, которые предназначены для принятия заказов и возврата данных клиенту. Служащие ассоциируются с большинством неконфиденциальных методов и объектов. Начальник – с методами и объектами, позволяющими получать конфиденциальную информацию, но не с методами, которые позволяют вносить изменения в работу, выполняемую персоналом. И список ролей, и их назначения могут изменяться со временем, причем делать это может администратор (без участия программиста).

Честно говоря, ролевая безопасность уступает по возможностям и гибкости парадигме работы с группами в NT, но благодаря простоте восприятия, легкости декларативной настройки и простоте программной проверки она существенно облегчает создание защищенных распределенных приложений.

Контекст вызова (call context)

Контекст вызова – это некоторая абстракция, через которую COM+ предоставляет свои сервисы серверному коду. Все нововведения в COM+ так или иначе связаны с контекстом вызова. Именно через этот самый контекст и осуществляется предоставление сервисов, связанных с защитой. COM+ предоставляет несколько интерфейсов, связанных с защитой. ISecurityCallContext позволяет легко получить расширенную информацию о текущем клиенте. IObjectContext в основном предназначен для управления транзакциями, но содержит также и методы, аналогичные ISecurityCallContext. Кроме них появился набор Automation-совместимых интерфейсов для работы из VB6 и скриптовых языков. Стоит отметить, что они значительно удобнее в использовании, чем их предшественники.

GetSecurityCallContext

Чтобы в VB получить доступ к свойствам контекста вызова, можно воспользоваться функцией GetSecurityCallContext. Она возвращает ссылку на объект SecurityCallContext, реализующий интерфейс ISecurityCallContext.

ISecurityIdentityColl

C и C++-программисты для тех же целей могут воспользоваться интерфейсом ISecurityIdentityColl. Для его получения нужно воспользоваться функцией CoGetCallContext.

ISecurityCallContext

Интерфейс ISecurityCallContext предоставляет доступ к методам защиты и контексту безопасности вызова. COM+-приложения, использующие ролевую безопасность, получают доступ к коллекции, содержащей свойства контекста вызова. С помощью этого интерфейса можно получить информацию о любом из участников цепочки вызовов.

Ниже перечислены свойства, содержащиеся в этой коллекции.

  • Direct Caller – клиент, непосредственно вызвавший метод сервера.
  • Original Caller – клиент, инициировавший цепочку вызовов.
  • Minimum Authentication Level – минимальный уровень аутентификации, используемый в цепочке вызовов.
  • Number of Callers – число участников цепочки вызовов.
  • Callers – возвращает коллекцию (ISecurityCallersColl), содержащую список всех участников цепочки вызовов.

Этот интерфейс также содержит методы IsSecurityEnabled (позволяющий узнать, включена ли ролевая безопасность), IsCallerInRole (позволяющий определить, назначена ли клиенту та или иная роль) и IsUserInRole, позволяющий определить, назначена ли пользователю та или иная роль.

IObjectContext

Этот интерфейс в в основном предназначен для управления транзакциями, но со содержит также и методы для работы с ролевой безопасностью. Взгляните на следующий код, взятый из метода GetInfo (серверного объекта из примера ComSec):

void TestIsCallerInRole(IObjectContext * pIObjectContext, 
 CComBSTR & sbsOutMsg, LPCTSTR szRole
)
{
 USES_CONVERSION;
 BOOL bInRole = FALSE;
 LPTSTR szMsg = NULL;
 TCHAR szBuf[1000];

 // Проверяем принадлежность ноли szRole.
 HRESULT hr = pIObjectContext->IsCallerInRole(CComBSTR(szRole), &bInRole);
 if(SUCCEEDED(hr))
 {
 // Формируем строку сообщения
 if(bInRole)
 szMsg = _T("\nВызывающая сторона в роли %s.");
 else
 szMsg = _T("\nВызывающая сторона НЕ (!) в роли %s.");

 // Подставляем в сформированную строку сообщения название роли
 wsprintf(szBuf, szMsg, szRole);
 sbsOutMsg.Append(szBuf);
 }
 else
 {
 szMsg = _T("\nОшибка: IsCallerInRole при проверке на роль %s");
 wsprintf(szBuf, szMsg, szRole);

 TestAppendErrorText(hr, szBuf, sbsOutMsg);
 }
}

...
CComPtr<IObjectContext> spIObjectContext;

HRESULT hr = GetObjectContext(&spIObjectContext);
if(SUCCEEDED(hr))
{
 sbsMsg.Append(OLESTR("Object-Context присутствует!"));
 
 // Проверяем, включена ли ролевая безопасность.
 if(spIObjectContext->IsSecurityEnabled())
 sbsMsg.Append(OLESTR("\nРолевая безопасность COM+ включена!\n"));
 else
 sbsMsg.Append(OLESTR("\nРолевая безопасность COM+ ВЫКЛЮЧЕНА!\n"));
 
 // Проверяем принадлежность к ролям...
 TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test1"));
 TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test2"));
 TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test3"));

В этом коде сначала с помощью функции GetObjectContext получается указатель на IObjectContext, а затем в функции TestIsCallerInRole происходит проверка ассоциации роли с данным клиентом. Она осуществляется с помощью метода IObjectContext::IsCallerInRole. Этот метод получает имя роли через свой первый параметр, а через последний параметр возвращает булево значение. TRUE означает, что пользователю назначена указанная роль, а FALSE – что нет. Если указанной роли вообще нет в рамках COM+-приложения, возвращается соответствующая ошибка.

Интересной особенностью метода IsCallerInRole является то, что TRUE возвращается также и в случае, если ролевая безопасность выключена в настройках приложения (о том, как это сделать, написано выше). Проверить, включена ли в данный момент ролевая безопасность, можно с помощью метода IObjectContext::IsSecurityEnabled.

Динамические проверки принадлежности клиента к той или иной роли нужны, если метод COM-объекта осуществляет доступ к некоторому ресурсу, и разным клиентам нужно давать разные права доступа к этому ресурсу (или организовать любую другую сложную логику). Например, вы можете создать метод, который изменяет бухгалтерскую проводку. Этот метод должен позволять рядовым бухгалтерам исправлять данные только в текущем периоде, а главному бухгалтеру – в предыдущем. Вместо того, чтобы создавать дополнительный метод, можно программно проверить клиента на принадлежность к группе «Главный бухгалтер». После установки приложения у заказчика администратор сможет назначить некоторым пользователям роль главного бухгалтера, позволяя изменять данные прошлого периода, и, тем самым, спокойно работать над сдачей баланса в то время, как остальные бухгалтеры будут защищены от непреднамеренной (а может, и преднамеренной) порчи данных. Подчеркну еще раз, что программные проверки не должны быть доминирующими. К ним нужно прибегать, только если декларативные средства не дают необходимой свободы действий.

Преимущества программной проверки заключаются в том, что можно организовать сколь угодно сложную логику работы, и в том, что для изменения (добавления, удаления) этой логики не требуется изменять интерфейсы компонентов, а стало быть, не нужно перенастраивать работающую систему.

Интерфейс ISecurityProperty

Интерфейс ISecurityProperty позволяет получить из контекста объекта информацию о клиентах, то есть SID создавшего объект клиента, и SID клиента, вызывающего объект в текущий момент.

Методы GetDirectCreatorSID и GetOriginalCreatorSID использовать не стоит. Дело в том, что результат их использования в COM+ непредсказуем – они разрабатывались для MTS и сохранены для совместимости.

Пример ComSec демонстрирует работу методов интерфейса ISecurityProperty, преобразуя полученные SID в текстовый вид. Однако сам пример не рассчитан на множественные вызовы. Поэтому, чтобы получить разные результаты вызовов, вам придется самостоятельно изменить пример.

На практике чаще всего используется метод GetDirectCallerSID – в качестве простого способа получения SID клиента.

Подводные камни

Напоследок рассмотрим несколько часто встречающихся проблем и пути их решения.

В различных конференциях и форумах то и дело появляются вопросы типа – «Мой DCOM/COM+-сервер прекрасно работает на одной машине (обычно W2k или NT 4) и наотрез отказывается работать на удаленном сервере. Что я делаю не так и главное(!), как определить в чем проблема?».

Большинство таких вопросов вызваны непониманием, а значит неправильным обращением с системой безопасности COM/Windows. Причем конкретных проблем может быть довольно много. Что же нужно сделать, чтобы найти ошибки?

Обработка ошибок

Первое, что нужно сделать (и это даже не относится непосредственно к защите) – это пройтись по своему приложению (причем как по серверной части, так и по клиентской), и добиться того, чтобы все потенциальные ошибки (или места, где они могут появиться) были обработаны. Причем чем более параноидально вы будете подходить к этому вопросу, тем лучше. Вы будете смеяться, но есть обратная зависимость между качеством обработки ошибок и количеством проблем, связанных с DCOM.

Как минимум, за любым вызовом COM- или API-функции должен следовать код обработки ошибки. Если ошибка произошла, нужно извлечь ее описание и перезапаковать ее в COM-стиле. Оптимальный вариант – сформировать собственное сообщение об ошибке, и дописать в его конец сообщение, полученное от системы или COM. В любом случае всегда должен быть код, прерывающий работу метода и возвращающий управление вызывающей стороне. Этот код, как минимум, должен возвратить системную ошибку. В примерах, прилагаемых к этой статье, свои сообщения не создаются, но системные и COM-сообщения перенаправляются клиенту.

В клиентских частях тестовых приложений, прилагаемых к этой статье, любой вызов, который может возвращать COM-ошибку, запакован в макрос CHK(), созданный мной специально для тестовых проектов. Вот его описание:

#define CHK(hr) \
 do \
 { \
 HRESULT hr_tmp = hr; \
 if(FAILED(hr_tmp)) \
 { \
 TestDisplayError(hr_tmp, \
 _T("При выполнени: \nCHK(")_T(#hr)_T(");")); \
 return hr_tmp; \
 } \
 } \
 while(0)

Этот макрос анализирует (с помощью стандартного макроса FAILED) HRESULT передаваемый ему в качестве параметра и, если произошла ошибка, вызывает функцию TestDisplayError, которая извлекает и показывает сообщение об ошибке вместе с передаваемым ей сообщением. Сообщение формируется из текста вызываемой строки кода и сообщения об ошибке COM. Таким образом, пользователь видит код, вызвавший ошибку, и сообщение, описывающее эту ошибку. После того, как пользователь закроет окно сообщения, происходит выход из функции с возвратом HRESULT, содержащего ошибку. Это подразумевает, что данный макрос будет использоваться из функций, возвращающих HRESULT. Реализацию этого макроса и функций, используемых для извлечения информации об ошибках, можно найти в файле shared.h, который находится в прилагаемых примерах. Обработка ошибок в COM заслуживает отдельной статьи, которая, возможно, появится на страницах этого журнала и сайте www.rsdn.ru.

Данный макрос прекрасно подходит для тестов, но малопригоден для использования в конечных приложениях. Так что если вы будете использовать его для выявления ошибок, связанных с защитой в COM-приложениях, не забудьте впоследствии заменить его на код, выдающий информацию, пригодную для отображения конечному пользователю.

На сервере тоже не производится полнофункциональной закладки собственных сообщений. Сообщения системы и COM перенаправляются клиенту, но не обычным способом, а упакованными в общую строку сообщения. Это нужно для того, чтобы можно было продолжать работу теста после некритичной ошибки. В реальных приложениях, лучше перезапаковывать сообщение и передавать управление вызвавшему клиенту.

Избегайте излишней вложенности. Это упрощает код, а значит, уменьшает вероятность появления ошибки. Чтобы избежать излишней вложенности, можно пользоваться простенькой хитростью. Вместо проверки на успех (макросом SUCCEEDED), старайтесь проверять на ошибку (макросом FAILED). Мой пример не придерживается этого правила, исключительно из-за того, что мне нужно было продемонстрировать логику работы COM даже в ситуации, когда большинство функций завершаются неудачно.

Понятно что обработка ошибок дело скучное, но главное – помнить, что время, потраченное на это занятие, окупится сторицей на стадии отладки и при запуске системы в эксплуатацию. Так, во втором тесте, чтобы понять, в чем дело, мне хватило двадцати минут. А задававший вопрос программист маялся с ним несколько дней. А ведь единственное, что я сделал – это добавил обработку ошибок!

Уточнение причин...

Итак, вы добавили обработчики ошибок всюду, где это было возможно, переписали клиентское приложение и сервер на клиентскую машину (или зарегистрировали proxy для COM+-приложения), но при запуске получили сообщение об ошибке. Что делать? Первое, что нужно проверить – а загружается ли вообще сервер? Есть простой, но исключительно эффективный способ – добавьте некоторый звук на загрузку процесса в настройках подопытного сервера. Это можно сделать с помощью одного простого трюка. Можно ассоциировать звуковой сигнал с запуском приложения (в «Control Panel» > «Sounds and Multimedia Properties» > «Sound Events» > «Windows» > «Open program»). Если звука нет, то проблема или в правах активации (см. выше) или неправильных настройках DCOM по умолчанию.

Первое, что стоит сделать – это убедиться, включен ли на машине DCOM. В разделе, посвященном dcomcnfg, рассказано, как это сделать.

Если все в порядке, но сервер все же не запускается, или запускается (звук есть), но не работает, следует проверить, на чем, собственно, вы пытаетесь запускать сервер и клиентское приложение?

Первое правило – не используйте в качестве сервера системы на базе Windows 9x. Даже под страхом увольнения. Если уж обстоятельства таковы, что не в вашей воле выбирать, и нужно использовать Windows 9x, даже не надейтесь работать с защитой. Постарайтесь ее отключить. В крайнем случае, знайте, что дескрипторы защиты незнакомы Windows 95. Прочтите все, что сможете найти про IAccessControl, и приготовьтесь к большому количеству разочарований. Но намного правильнее будет показать эти строки своему начальству и попросить денег на покупку W2k (или одного из ее отпрысков).

Используя Windows 9x в качестве клиента, не забудьте установить DCOM 9x 1.3 или более совершенную версию. Учтите, что без установки этого пакета использовать Windows 9x лучше не стоит. Да и с DCOM 9x потомки Windows 95 проигрывают во всех отношениях (кроме объемов необходимой памяти) ОСям линейки NT.

Если в качестве ОС вы используете NT4, постарайтесь обеспечить, чтобы на ОС был установлен SP6a (или более свежий пакет обновления, если таковой появится). DCOM/MTS-приложения (и COM+-клиенты) будет работать и с более старым пакетом обновления. Но, во-первых, не все возможности будет доступны. А во-вторых, вы можете столкнуться с ненужными проблемами, которых легко избежать, потратив двадцать минут на установку SP6.

Даже с установленным SP6 NT4 все же уступает W2k. Так что если у вас есть выбор, то всегда выбирайте W2k.

Если сервер (установленный на NT4) не запускается, а клиенту возвращается ошибка E_OUTOFMEMORY, с большой долей уверенности можно сказать, что вы пользуетесь устаревшей версией dcomcnfg или OLEView, которые не добавляют учетную запись SYSTEM в права доступа (или права доступа по умолчанию). В правах доступа всегда должна присутствовать учетная запись SYSTEM. Это связано с тем, что запуск COM-объектов осуществляет COM SCM, и, естественно, если у него нет прав на доступ к объекту, ничего хорошего не выйдет. Странное же сообщение об ошибке (E_OUTOFMEMORY) является всего лишь банальной ошибкой в DCOM (которая исправлена в более новых версиях DCOM, но вам от этого легче не станет).

Проверьте, не зарегистрирован ли ваш сервер через сетевой путь. Это тоже может вызвать проблему. Лучше всего регистрировать серверы в локальной директории, имеющей короткие имена и небольшую вложенность.

Если вы получаете сообщение E_ACCESSDENIED, сначала нужно убедиться в том, что оба компьютера находятся в одном домене, или что домены, к которым они относятся, соединены доверительными отношениями. При этом нужно пользоваться именно учетными записями домена, а не локальными. Частой ошибкой является использование некоторой локальной учетной записи. Надо понимать, что, например, учетные записи Administrator на одной машине и на другой – это две совершенно разные записи. Administrator на машинах VLAD и MIKE, и администратор домена OPT будут восприниматься подсистемой защиты NT как VLAD\Administrator, MIKE\Administrator и OPT\Administrator! Если домена вообще нет (и только в этом случае!), можно воспользоваться предоставляемой NT возможностью защиты на уровне рабочих групп. Основная идея заключается в том, что если учетные записи на разных машинах имеют одинаковые имя и пароль, то NT будет автоматически авторизовать доступ с удаленного компьютера, как будто он производится от имени локальной учетной записи. Но это происходит, только если компьютеры не подключены к домену.

Вообще, если есть возможность использовать доменную подсистему NT, это делать нужно обязательно. Как минимум, вы выиграете в скорости.

Если создание объекта и вызов его методов выполняются с другого сервера, то убедитесь, что учетной записи, под которой запущен этот другой сервер, даны права на активацию и доступ к первому серверу.

Если вы получили сообщение об ошибке, говорящее, что User32.dll не инициализирована или не может быть загружена, знайте, что ваша программа пытается вывести окно, послать сообщение или сделать еще что-либо с десктопом, но у учетной записи, под которой она запущена, недостаточно прав на взаимодействие с Windows-станцией или десктопом.

Если в сообщении фигурирует Shell32.dll, скорее всего, приложение запущено под учетной записью пользователя, которому запрещен локальный доступ в систему, а приложение пытается обратиться к его профайлу.

Локализовать проблему, возникающую при вызове, можно следующим образом.

Используйте отладчик, чтобы проверить, достигает ли вызов серверного метода. Если этого не происходит, проанализируйте ошибку, возвращаемую при вызове. Проблема может быть, например, в несоответствии уровней аутентификации или авторизации. Проблема несоответствия уровня аутентификации особенно обостряется, если клиентом (или еще хуже, сервером) выступает Windows 9x. В этом случае при вызовах методов сервера проблемы беспокоят не так сильно, но если возникает необходимость произвести обратный вызов (например, принять оповещение клиента о событии), без ручного вмешательства в настройки защиты не обойтись. Причем в большинстве случаев обратные вызовы совершенно не нужно защищать (ведь подключаться к интерфейсам клиента не только сложно, но и довольно бессмысленно).

Я решил рассмотреть вопрос с обратными вызовами более подробно. Для этого к статье прилагается тестовый проект ComSrvEvents, в котором имеется DCOM-сервер с объектом, реализующим события (через IConnectionPoint/ IConnectionPointContainer), и простенький клиент, подключающийся к событиям сервера и вызывающий его метод Method1. Этот метод инициирует рассылку уведомлений (обратных вызовов).

Первая версия этого теста не предпринимала никаких действий и вылетала с ошибкой при попытке подключиться к событиям сервера. Реально сбой происходил при попытке вызова QueryInterface внутри реализации серверного метода IConnectionPoint::Advise. Причем проблема проявлялась только при запуске DCOM-приложения на удаленном сервере (локально все работало).

Что делать в такой ситуации?

Есть два пути. Первый – отключить (понизить до минимума установки) защиту на сервере и клиенте. Это можно сделать, вызвав на сервере и клиенте функцию CoInitializeSecurity следующим образом:

CoInitializeSecurity(NULL, -1, NULL, NULL,
 RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IDENTIFY, 
 NULL, EOAC_NONE, NULL);

Главное здесь – минимальный уровень аутентификации. Так как уровень аутентификации и у клиента, и у сервера по умолчанию NONE, никаких проверок прав доступа производиться не будет, и следовательно, вызовы в обе стороны будут производиться без пробоем.

Но такой вариант подходит, только если вам глубоко наплевать на защиту и попросту нужно от нее избавиться. Если защита для сервера все же нужна, то можно понизить или отключить защиту для клиента, но оставить достаточный уровень защиты сервера. Можно проинициализировать защиту приведенным выше образом только для клиента. Но при попытке обратного вызова (того же самого QueryInterface в IConnectionPoint::Advise) вы снова получите сообщение об ошибке. Что же происходит? Дело в том, что вне зависимости от того, делается прямой вызов или обратный, уровни аутентификации и имперсонации подбираются COM-ом путем анализа установок клиента и сервера. Этот процесс подробно описан выше. При этом клиентский объект вызывается не с минимальными настройками, а с настройками, заданными при вызове функции CoInitializeSecurity на сервере. Чтобы избавиться от этого эффекта, можно задавать настройки этих параметров напрямую для proxy клиентского интерфейса (непосредственно перед вызовом). Сервер из проекта ComSrvEvents реализован на ATL. ATL предоставляет визард, помогающий упростить реализацию IConnectionPoint. Этот визард предлагает выбрать библиотеку типов и имя интерфейса, и по описанию этого интерфейса генерирует файл, содержащий класс, реализующий IConnectionPoint для выбранного интерфейса.

Основная реализация IConnectionPoint находится в шаблоне IConnectionPointImpl<> (от которого наследуется генерируемый класс). В этом шаблоне находится и реализация метода IConnectionPoint::Advise. Чтобы понизить уровень аутентификации и другие параметры, я переопределил метод Advise в сгенерированном визардом классе. Вот что из этого получилось:

STDMETHODIMP Advise(IUnknown* pUnkSink, DWORD* pdwCookie)
{
 T* pT = static_cast<T*>(this);
 IUnknown* p;
 HRESULT hRes = S_OK;
 if (pUnkSink == NULL || pdwCookie == NULL)
 return E_POINTER;

 IID iid;
 GetConnectionInterface(&iid);

 CoSetProxyBlanket(pUnkSink, 
 RPC_C_AUTHN_NONE, 
 RPC_C_AUTHZ_NONE, 
 NULL,
 RPC_C_AUTHN_LEVEL_NONE,
 RPC_C_IMP_LEVEL_IDENTIFY,
 NULL,
 EOAC_NONE);

 hRes = pUnkSink->QueryInterface(iid, (void**)&p);
 if (SUCCEEDED(hRes))
 {
 pT->Lock();
 *pdwCookie = m_vec.Add(p);
 hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
 pT->Unlock();
 if (hRes != S_OK)
 p->Release();
 }
 else if (hRes == E_NOINTERFACE)
 hRes = CONNECT_E_CANNOTCONNECT;
 if (FAILED(hRes))
 *pdwCookie = 0;
 return hRes;
}

Единственное изменение, которое мне пришлось сделать –добавить вызов CoSetProxyBlanket (выделено жирным).

Такой же код пришлось добавить в методы рассылки сообщений, сгенерированные визардом:

HRESULT Fire_Event1(BSTR str)
{
 CComVariant varResult;
 T* pT = static_cast<T*>(this);
 CComVariant pvars[1];
 int nConnections = m_vec.GetSize();
 
 int nConnectionIndex = 0;
 for (; nConnectionIndex < nConnections; nConnectionIndex++)
 {
 pT->Lock();
 CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
 pT->Unlock();
 IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
 if (pDispatch != NULL)
 {
 VariantClear(&varResult);
 pvars[0] = str;
 DISPPARAMS disp = { pvars, NULL, 1, 0 };

#ifdef _TEST_WITH_SERVER_SECURE
 CoSetProxyBlanket(pDispatch, 
 RPC_C_AUTHN_NONE, 
 RPC_C_AUTHZ_NONE, 
 NULL,
 RPC_C_AUTHN_LEVEL_NONE,
 RPC_C_IMP_LEVEL_IDENTIFY,
 NULL,
 EOAC_NONE);
#endif // _TEST_WITH_SERVER_SECURE

 HRESULT hr = pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT,
 DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
 if(FAILED(hr))
 if(DISP_E_EXCEPTION != hr)
 { // Это исправление бага ATL-визарда. 
 // Без этого исправления ошибки при обратных вызовах
 // попросту "замалчиваются"!
 varResult.scode = hr;
 break;
 }
 }
 }
 return varResult.scode;

}

Я также исправил ошибку, вносимую визардом – некорректную обработку возвращаемого значения метода Invoke.
Этих действий достаточно, чтобы заставить сервер правильно работать с клиентами, запускаемыми под различными ОС (в том числе и под Windows 9x), и при этом не отказываться от защиты на сервере.
Если совсем ничего не получается, откройте Event Log. Многие ошибки, возникающие при работе COM, помещаются именно туда.
Следует включить аудит событий logon/logoff. Это позволит определить, под какой учетной записью клиент обращается к серверу, и, если проблема в аутентификации (например, учетная запись клиента неизвестна на серверном компьютере, или задан неверный пароль), это сразу же проявится.
Если же ничего из перечисленного выше не помогло, придется искать помощь на стороне. Например, вы можете обратиться в форум COM/DCOM/COM+ наwww.rsdn.ru. Возможно, ваш вопрос дойдет. :)

Внаглую стырено с древнего Источника: RSDN Magazine

Категория: Службы и консоли | Добавил: masterov (01.12.2017) | Автор: Андрей Мастеров E W
Просмотров: 29 | Теги: Windows, службы | Рейтинг: 0.0/0
Всего комментариев: 0
avatar