Разграничение доступа из кода в WCF

ОГЛАВЛЕНИЕ

Представленное в Microsoft .NET Framework 1.0 разграничение доступа из кода (CAS) является, вероятно, единственной возможностью, отличающей .NET от неуправляемого кода. CAS встроено в самую основу .NET Framework, влияя на каждую операцию в управляемом коде, что просто недостижимо для неуправляемого кода.

Первый выпуск Windows Communication Foundation (WCF) не поддерживал CAS; сборка System.ServiceModel не допускала частично доверенные источники вызовов, что отключало поддержку CAS. Второй выпуск представил рудиментарную поддержку CAS в некоторых из привязок HTTP, причем только для ограниченного набора случаев. Это изменение позволило мне написать небольшую инфраструктуру, предоставляющую всестороннюю поддержку для CAS, не внося нарушений ни в модель программирования WCF, ни в CAS.

В этой первой из двух статей о CAS я кратко расскажу о разграничении доступа из кода в WCF и затем покажу свое решение по допуску частично доверенных клиентов в службах WCF.


Первый взгляд на CAS

.NET Framework определяет 24 различных полномочия безопасности, управляющие работой почти любого типа. Существуют полномочия для файлового ввода/вывода, интерфейса пользователя, безопасности, работы с сетью, доступа к данным и так далее. Тип полномочия может быть применен к определенному ресурсу, скажем полномочие читать из определенного файла в случае полномочия для файлового ввода/вывода или полномочия отображать определенные типы окон в случае полномочия для интерфейса пользователя. В полномочии также может быть полностью отказано (скажем, никаких операций файлового ввода/вывода вообще), либо оно может быть всеобъемлющим (скажем, неограниченный доступ на ввод/вывод из файлов).

Полномочия сгруппированы в наборы полномочий, и каждой сборке назначается свой набор. .NET Framework определяет некоторые стандартные наборы полномочий, такие как FullTrust («Полное доверие», подразумевает все полномочия) или Execution («Исполнение», полномочие только на доступ к ЦП). Администраторы могут использовать средство настройки .NET для определения собственных наборов полномочий, а разработчики могут определять нестандартные наборы полномочий программно – используя файл набора полномочий или определяя манифест приложения ClickOnce с набором полномочий, которых требует приложение.

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

Каждой области приложения всегда назначается набор полномочий, именуемых политикой безопасности области приложения, и любая сборка, загружаемая в данный момент, попадает под действие этого набора – в противном случае она сталкивается с исключением безопасности. Новые области приложений запускаются с набором полномочий FullTrust, а поскольку весь код, происходящий с локальной машины, также по умолчанию получает FullTrust, большинство приложений на основе .NET работают только в своей исходной настройке, по сути не используя CAS вообще. Это делает программный год (а также пользователя, данные, компьютер и даже сеть) подверженным множеству несчастий – от преодоления безопасности вирусами или червями до простых ошибок пользователя.

Код, исполняющийся с иным уровнем доверия, чем полный, именуется частично доверенным кодом. Когда любая часть управляемого кода пытается получить доступ к ресурсу или выполнить операцию, используя.NET Framework (включая взаимодействие с управляемым кодом), .NET Framework проверяет наличие у сборки, содержащей этот код, необходимых полномочий на выполнение операции. Если у сборки отсутствует необходимое полномочие, .NET Framework выдает исключение безопасности, тем самым отменяя операцию.

Поскольку менее доверенная сборка, содержащая вредоносный код, может «заманить» доверенную сборку в выполнение операций, на выполнение которых у менее доверенной сборки нет полномочий, требовать полномочия только от сборки, выполняющей операцию, недостаточно. Поэтому .NET Framework перебирает весь стек источников вызова, убеждаясь, что каждый источник выше по стеку имеет необходимые полномочия. Этот анализ стека именуется требованием безопасности и выполняется вне зависимости от полномочий выполняющейся сборки.

Код может также подтвердить полномочие – то есть, заявить, что каждый источник вызова выше по стеку имеет требуемые полномочия. Подтверждение полномочия останавливает анализ стека. Код может подтверждать только те полномочия, которые он уже имеет, и только при условии, что у него есть специальное полномочие подтверждения безопасности.

При подтверждении одного полномочия всегда стоит требовать другого вместо него. Разработчики могут требовать или подтверждать полномочия программно, используя специальные классы полномочий или набор совпадающих атрибутов. Разработчики могут также активно отказывать в полномочиях на уровне сборки, класса или метода. Отказ в полномочиях или допуск лишь ограниченного набора полномочий, требуемых для выполнения кода, уменьшает область, открытую для атаки с приманкой. Дополнительные сведения о разграничении доступа из кода приведены в главе 12 второго издания моей книги «Programming .NET Components» («Программирование компонентов .NET»), где этой фундаментальной технологии и ее применениям посвящено более 100 страниц.


Клиентское CAS

В .NET Framework 3.5, WCF допускает исполнение на условиях частичного доверия лишь для ограниченного набора случаев. WCF допускает только вызов на условиях частичного доверия лишь таких привязок HTTP, как BasicHttpBinding, WSHttpBinding и WebHttpBinding (исключая WSDualHttpBinding) и либо вообще безо всякой безопасности, либо только с безопасностью передачи данных. Более того, в случае WSHttpBinding не допускаются такие аспекты, как безопасность сообщений, надежный обмен сообщениями и транзакции. Все привязки со включенным частичным доверием должны использовать текстовое кодирование. Клиент не может использовать такие дополнительные возможности WCF как диагностику. Для включения использования в частично доверенной среде сборка System.ServiceModel допускает источники вызова с частичным доверием, в том числе аттрибут AllowPartiallyTrustedCallers в определение сборки:

[assembly: AllowPartiallyTrustedCallers]

В первом выпуске WCF пропуск этого атрибута исключал любое использование частичного доверия. В .NET Framework 3.5 принудительное применение ограниченного набора поддерживаемых функций является обязанностью привязок. Каждая привязка, кроме привязок HTTP, активно требует полного доверия ее источников вызова, будь это прокси клиента или место размещения службы. Это позволяет самим привязкам HTTP не требовать полного доверия, а вместо этого требовать полномочий в соответствии с контекстом использования. На стороне клиента эти привязки требует полномочий на выполнение (полномочий безопасности с флагом исполнения) и полномочий на подключение к службе (веб-полномочий с флагом подключения к целевому URI-адресу).

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

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


Частично доверенные клиенты

Чтобы позволить клиентам на любом уровне частичного доверия использовать любые из компонентов и привязок WCF, необходимо заблокировать требование полного доверия со стороны привязок. Единственный способ сделать это – заставить сам прокси установить полное доверие. Установку полного доверия можно проделать через PermissionSetAttribute, используя флаг Assert («Подтвердить») перечисления SecurityAction и указывая строго типизированную строку "FullTrust" для имени полномочия:

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]

Хотя имя полномочия указывается с использованием строки, компилятор добавляет определенную степень безопасности, проверяя допустимые значение.

Вдобавок, необходимо предотвратить прямой доступ клиента к любому методу базового класса ClientBase<T> (который все так же требует полного доверия), так что прокси необходимо скрыть часто используемые методы Close («Закрыть») и Dispose («Удалить»). То, что сам класс прокси может получать доступ к методам или свойствам ClientBase<T> (таким, как Channel («Канал») или конструкторы) вполне допустимо, поскольку прокси устанавливает полное доверие.

Проблема заключается в том, что для установки полного доверия самому прокси должно быть дано полное доверие, а клиент, имеющий только частичное доверие, не может предоставить это. Следовательно, класс прокси необходимо вынести в собственную сборку, пометить ее как общедоступную и дать этой сборке полное доверие. Это можно сделать, используя приложение панели управления настройки .NET Framework 2.0 – просто укажите сборку прокси, используя основанные на содержимом доказательства, такие как строгое имя, и дайте сборке полное доверие.

Можно также установить сборку прокси в глобальном кэше сборок (GAC) клиента. Поскольку всем сборкам, исходящим из GAC, дается полное доверие, прокси также будет дано полное доверие. Нужно также не забыть объявить атрибут AllowPartiallyTrustedCallers, это позволит частично доверенным источникам вызовов вызывать сборку.

Наконец, необходимо добавить используемое прокси определение контракта к сборке прокси (а также пометить контракт как общедоступный). Это необходимо, поскольку WCF требует полного доверия всех сборок вверх по цепи вызова, и если контракт исходит от частично доверенной сборки, то требование кончится неудачей. На рис. 1 показаны примеры этих определений контракта и прокси.

Рис. 1 Установка полного доверия с помощью прокси

[assembly: AllowPartiallyTrustedCallers]
 
[ServiceContract]
public interface IMyContract
{
  [OperationContract]
  void MyMethod();
}

 

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
public class MyContractClient :
  ClientBase<IMyContract>,IMyContract,IDisposable
{
  public MyContractClient() {}

  public MyContractClient(string endpointName) : base(endpointName) {}

  /* More constructors */ 

  public void MyMethod() {
   Channel.MyMethod();
  }
  public new void Close() {
   base.Close();
  }
  void IDisposable.Dispose() {
   Close();
  }
}


Требования клиента

Увы, прием, показанный на рис. 1, представляет собой потенциальную брешь в защите. Любой частично доверенный клиент теперь может вызвать службу WCF, подавляя требования безопасности WCF. Представьте себе клиента, которому не было дано полномочий на подключение к сокету TCP или веб-узлу. Хотя этот клиент и не может использовать сеть напрямую, он может обойти это ограничение, делая вызов из ограниченной клиентской среды через WCF.

Решение для этой проблемы – использование специального подкласса ClientBase<T>, который, с одной стороны, установит чистое требование полного доверия от WCF и, с другой стороны, потребует определенных полномочий безопасности в соответствии с тем, что пытается сделать клиент. Таким классом прокси является мой класс PartialTrustClientBase<T>, показанный на рис. 2.

Рис. 2 Класс PartialTrustClientBase<T>

public abstract class PartialTrustClientBase<T> : 
  ClientBase<T>,IDisposable 
  where T : class
{
  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase() {}

 

  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase(string endpointName) : 
   base(endpointName) {}

  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase(Binding binding,
   EndpointAddress remoteAddress) : 
   base(binding,remoteAddress) {}

  //Useful only for clients that want full-brunt raw demands from WCF
  protected new T Channel
  {
   [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
   get {
    return base.Channel;
   }
  }
  
  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  new public void Close() {
   base.Close();
  }
  void IDisposable.Dispose() {
   Close();
  }

  protected object Invoke(string operation,params object[] args) {
   if(IsAsyncCall(operation)) {
    DemandAsyncPermissions();
   }
   DemandSyncPermissions(operation);
   CodeAccessSecurityHelper.PermissionSetFromStandardSet(
    StandardPermissionSet.FullTrust).Assert();

   Type contract = typeof(T);
   MethodInfo methodInfo = contract.GetMethod(operation);
   return methodInfo.Invoke(Channel,args);
  }

  protected virtual void DemandAsyncPermissions() {
   CodeAccessSecurityHelper.DemandAsyncPermissions();
  }

  protected virtual void DemandSyncPermissions(string operationName) {
   this.DemandClientPermissions(operation);
  }

  bool IsAsyncCall(string operation) {
   if(operation.StartsWith("Begin")) {
    MethodInfo info = typeof(T).GetMethod(operation);
    object[] attributes = info.GetCustomAttributes(
     typeof(OperationContractAttribute),false);
    Debug.Assert(attributes.Length == 1);
    return (attributes[0] as 
     OperationContractAttribute).AsyncPattern;
   }
   return false;
  }
}

PartialTrustClientBase<T> используется подобно обычному базовому классу прокси. Его производному классу прокси тоже необходимо дать полное доверие и разрешить источники вызова с частичным доверием. Однако, в отличие от показанного на рис. 1, PartialTrustClientBase<T> не утверждает полное доверие на уровне класса. Вместо этого он утверждает полное доверие локально, лишь когда это требуется. Вдобавок, PartialTrustClientBase<T> также может быть использован, чтобы требовать другие полномочия CAS.

Если произвести класс прокси PartialTrustClientBase<T> и заставить прокси установить полное доверие, как показано здесь, вызывающему клиенту не будет предъявляться требований:

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
public class MyContractClient : 
  PartialTrustClientBase<IMyContract>,IMyContract
{
  public MyContractClient() {}

 

  public MyContractClient(string endpointName) : 
   base(endpointName) {}

  public void MyMethod() {
   Channel.MyMethod();
  }
}

Единственная разница между этим кодом и рис. 1 заключается в том, что новая версия чище, поскольку сокрытие Close и Dispose теперь выполняется PartialTrustClientBase<T>. Этот новый прокси по-прежнему подавляет все требования безопасности от WCF и оставляет частично доверенного клиента открытым для атаки с приманкой или позволяет ему делать больше, чем положено.


Необработанные требования WCF

Более учитывающее безопасность использование PartialTrustClientBase<T> – это не дать прокси установить полное доверие:

public class MyContractClient : 
  PartialTrustClientBase<IMyContract>,IMyContract
{
  public MyContractClient() {}
  public MyContractClient(string endpointName) : 
   base(endpointName) {}
  public void MyMethod() {
   Channel.MyMethod();
  }
}

Чтобы поддержать это, PartialTrustClientBase<T> точечно подтверждает полное доверие для своих конструкторов и своего метода Close. Вдобавок, PartialTrustClientBase<T> скрывает свойство Channel класса ClientBase<T> и устанавливает полное доверие на методе получения. Этого достаточно, чтобы подавить требование полного доверия со стороны привязки WCF, поскольку это требование выдается при конструировании прокси, а также при открытии и закрытии его канала, но не при собственно использовании. Интересным эффектом структурирования прокси таким образом является открытие кода клиента необработанным требованиям безопасности WCF – то есть всем требованиям безопасности, требуемым для передачи вызова службе!

Например, если прокси использует привязку TCP, прокси сперва запросит полномочия клиента на исполнение (всему управляемому коду требуется это полномочие). Далее, прокси потребует от клиента полномочия на подключение к требуемому порту на компьютере службы и неограниченных полномочий DNS (необходимых для полномочия адреса узла). Кроме того, возникают некоторые побочные требования полномочий, относящиеся не к использованию TCP, а к контексту вызова.

Если клиент желает использовать безопасность Windows и отправить интерактивное удостоверение пользователя, то прокси потребует полномочие среды на доступ к переменной USERNAME. Если клиент желает отправить альтернативные учетные данные Windows, то прокси потребует полномочие безопасности на управление участником. Если клиент желает отправить вызов асинхронно или получить дуплексные обратные вызовы, то прокси потребует полномочие на управление политикой и доказательством (и то и другое является флагами полномочия безопасности, требуемыми при «прыжках» вызовов между потоками). Если клиент желает использовать надежный обмен сообщениями, то прокси потребует также и контроля над политикой. Если клиент использует защиту сообщений с помощью учетных данных имени пользователя и согласования сертификата службы, но без проверки согласованного сертификата службы, прокси также потребует полномочия на управление политикой и доказательством. Если клиент использует возможности диагностики и отслеживания WCF, то прокси потребует доступа к переменной среды COMPUTERNAME (чтобы иметь возможность отслеживать ее) и доступ неуправляемого кода (предположительно для доступа к файлам журнала, так что это должно было бы быть полномочие на ввод/вывод).

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

Определенные возможности WCF также без затей переходят к требованиям полного доверия, несмотря на наличие отлично подходящих типов полномочий. Любая попытка распространить транзакцию требует полного доверия (вместо использования полномочия распределенной транзакции), равно как и любой доступ к хранилищу сертификатов (вместо использования полномочия хранилища сертификатов). На рис. 3 показаны необработанные требования безопасности WCF, вызываемые кодом, вроде моего последнего варианта PartialTrustClientBase<T>, когда привязкам установлены их параметры по умолчанию для нескольких ключевых случаев, таких как транзакции, надежность, диагностика, асинхронные вызовы, доступ к хранилищу сертификатов и защита сообщений.

Рис. 3 Необработанные клиентские требования безопасности WCF

     
СлучайПолномочия
TCPПолномочие безопасности с исполнением и неуправляемым кодом, неограниченным DNS, полномочием сокету подключаться к порту на целевом узле
IPCПолномочие безопасности с исполнением, неуправляемым кодом, управлением политикой и доказательствами
WS и WS DualПолномочие безопасности с исполнением и неуправляемым кодом, веб-полномочие на подключение к URI
Основные и вебПолномочие безопасности с исполнением, веб-полномочие на подключение к URI
MSMQПолномочие безопасности с исполнением и неуправляемым кодом
Асинхронные вызовы, дуплекс через TCPПолномочие безопасности с управлением политикой и доказательствами
RM через TCPПолномочие безопасности с управлением политикой
Безопасность Windows с интерактивными учетными данными пользователяПолномочие среды на чтение USERNAME
Безопасность Windows с альтернативными учетными даннымиПолномочие безопасности на управление участником, полномочие среды на чтение USERNAME
Диагностическое отслеживаниеПолномочие безопасности с неуправляемым кодом, полномочие среды на чтение COMPUTERNAME
Учетные данные имени пользователя, безопасность сообщений с согласованием сертификата службы, но без его проверкиПолномочие безопасности на управление политикой и доказательствами, полномочие хранилища на перечисление сертификатов
Учетные данные имени пользователя, безопасность сообщений TCP с согласованием сертификата службы, но без его проверкиПолномочие безопасности с управлением политикой и доказательствами
Доступ к любому хранилищу сертификатов, распространение транзакцийПолное доверие
Учетные данные имени пользователя, безопасность сообщений без согласования сертификата службы или с его проверкой, учетные данные сертификатаПолное доверие
 

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


Требования, структурированные с помощью PartialTrustClientBase<T>

Как можно увидеть на рис. 3, ключевые аспекты WCF, такие как транзакции, надежный обмен сообщениями, неограниченное взаимодействие с безопасностью сообщений и доступ к хранилищу сертификатов, как один требуют полного доверия, делая мою предыдущую версию PartialTrustClientBase<T> бессмысленной в среде частичного доверия. Более того, требование доступа неуправляемого кода практически всеми привязками, кроме привязок HTTP (и привязок WS с защитой сообщений), недопустимо, поскольку уничтожает саму идею CAS для частично доверенного клиента.

Чтобы обеспечить адекватное, безопасное и верное использование WCF частично доверенным клиентом, PartialTrustClientBase<T> предлагает метод Invoke («Вызвать»), определенный как:

protected object Invoke(string operation,params object[] args);

Invoke принимает имя операции, которую следует вызвать, и ее параметры в форме разделенного запятыми массива объектов params. Вместо использования свойства Channel для совершения вызова, Invoke использует отражение. Вот прокси, произведенный от PartialTrustClientBase<T> с использованием метода Invoke:

public class MyContractClient : 
  PartialTrustClientBase<IMyContract>,IMyContract 
{
  public MyContractClient() {}
  public MyContractClient(string endpointName) : 
   base(endpointName) {}
  public void MyMethod() {
   Invoke("MyMethod");
  }
}

Invoke сперва потребует нужных полномочий CAS в соответствии с вариантом вызывающего клиента и конечной точкой целевой службы. Если клиенту будут даны эти полномочия, то есть если требования не вызовут исключения безопасности, Invoke программно установит полное доверие и приступит к вызову операции, удовлетворенный наличием у клиента верных полномочий на вызов службы. Такое поведение я зову структурированным требованием полномочия.
Invoke не может использовать атрибут для декларативной установки полного доверия, поскольку это замаскирует любые более детализированные полномочия, которые он требует. Вместо этого реализация Invoke на рис. 2 сперва проверяет, не произведено ли обращение к вызову асинхронно (используя флаг AsyncPattern контракта операции). Если это так, Invoke требует соответствующих полномочий, используя вспомогательный метод DemandAsyncPermissions. После этого Invoke требует синхронных полномочий, используя вспомогательный метод DemandSyncPermissions. Для самого вызова Invoke программно устанавливает полное доверие, используя мой класс CodeAccessSecurityHelper, показанный на рис. 4.

Рис. 4 Класс CodeAccessSecurityHelper

public enum StandardPermissionSet {
  Internet,
  LocalIntranet,
  FullTrust,
  Execution,
  SkipVerification
}

 

public static class CodeAccessSecurityHelper {
  public static PermissionSet PermissionSetFromStandardSet(
   StandardPermissionSet standardSet);
   
  public static void DemandClientPermissions<T>(this ClientBase<T> proxy) 
   where T : class;

  public static void DemandAsyncPermissions();
  
  //More members

Метод PermissionSetFromStandardSet берет мое значение перечисления, представляющее один из стандартных наборов полномочий.NET, и возвращает подходящий экземпляр PermissionSet (см. рис. 5). PermissionSet, как и предполагает его имя, является коллекцией полномочий, но может также представлять сверхнабор полномочий полного доверия (который больше похож на единственное полномочие, чем на набор отдельных полномочий). Класс PermissionSet поддерживает интерфейс IStackWalk, позволяющий установить модификатор анализа стека, скажем модификатор, останавливающий требование к полномочиям в наборе полномочий, утверждая, что у всех источников вызова выше по стеку имеются эти полномочия. Модификатор анализа стека удаляется автоматически при возвращении из установившего его метода. Его можно также удалить напрямую с помощью статического метода RevertAssert набора PermissionSet.

Рис. 5 PermissionState и PermissionSet

public enum StandardPermissionSet {
  Internet,
  LocalIntranet,
  FullTrust,
  Execution,
  SkipVerification
}

 

public static class CodeAccessSecurityHelper {
  public static PermissionSet PermissionSetFromStandardSet(
   StandardPermissionSet standardSet);
   
  public static void DemandClientPermissions<T>(this ClientBase<T> proxy) 
   where T : class;

  public static void DemandAsyncPermissions();
  
  //More members

Вспомогательные методы DemandAsyncPermissions и DemandSyncPermissions класса PartialTrustClientBase<T> используют соответствующие методы CodeAccessSecurityHelper для выполнения своих требований.

На диаграмме на рис. 6 показаны структурированные требования, создаваемые PartialTrustClientBase<T>.Invoke в качестве функции использованной привязки, и другие аспекты ситуации, такие как использование транзакций, надежности, доступа к хранилищу сертификатов, диагностики, обратных вызовов и асинхронных вызовов.

Рис. 6 Структурированные требования безопасности PartialTrustClientBase<T>

     
СлучайПолномочия
TCPПолномочие безопасности с исполнением, неограниченным DNS, полномочием сокету подключаться к порту на целевом узле
IPCПолномочие безопасности с исполнением, управлением политикой и доказательствами
WS, БазовыйПолномочие безопасности с исполнением и веб-полномочие на подключение к URI
WS-DualПолномочие безопасности с исполнением и веб-полномочие на подключение к URI, а также веб-полномочие на прием обратных вызовов на адрес обратного вызова, минимальное полномочие на размещение ASP.NET.
MSMQПолномочие безопасности с исполнением, полномочие MSMQ на отправку запроса
RM через TCPПолномочие безопасности с управлением политикой
Учетные данные имени пользователя, дуплекс через TCP, асинхронные вызовы(AsyncPattern), защита сообщений с согласованием сертификата службы, но без его проверкиПолномочие безопасности с управлением политикой и управлением доказательствами
Безопасность Windows с интерактивными учетными данными пользвателяПолномочие среды на чтение USERNAME
Безопасность Windows с альтернативными учетными даннымиПолномочие безопасности на управление участником
Распространение транзакцийНеограниченное полномочие распределенных транзакций
Учетные данные имени пользователя, защита сообщений без согласования сертификата службы или с его проверкой, учетные данные сертификата Полномочие хранилища на перечисление хранилищ, открытие хранилища и перечисление сертификатов
Диагностическое отслеживаниеПолномочие среды на чтение COMPUTERNAME, полномочие файлового ввода/вывода на обнаружение пути, добавление, запись в файлы журнала
 


Анализ требований Invoke

Я основал структурированные требования на нескольких элементах. Во-первых, когда это было возможно, я пытался приблизительно представить необработанные требования на клиенте, порождаемые WCF. Впрочем, я все же сгладил острые края WCF, поскольку эта платформа не была разработана для всестороннего использования в условиях частичного доверия. Привязки и случаи, перечисленные на рис. 6, не требуют полного доверия или доступа неуправляемого кода. Довольно многие части .NET Framework были разработаны для использования в среде частичного доверия и в подобном контексте, и я полагался на те же требования, что и они. Наконец, в ряде случаев, чтобы подавить требования полного доверия, я полагался на свой опыт, знакомство с CAS и здравый смысл, сопоставляя действия WCF с требованиями для специальных типов полномочий.

Когда используется привязка WS-Dual, Invoke требует веб-полномочия на подключение к целевой конечной точке, как и в случае со многими другими привязками HTTP. Однако, чтобы позволить размещение объекта обратного вызова, он также требует минимального полномочия ASP.NET на размещение и веб-полномочия на прием вызовов к адресу обратного вызова. При использовании привязки MSMQ Invoke требует полномочия MSMQ на отправку сообщений целевой очереди.

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

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

Когда клиент использует диагностику WCF, Invoke требует полномочия среды на чтение имени компьютера и полномочия на файловый ввод/вывод для используемых файлов журнала и трассировки.


Реализация структурированных требований на стороне клиента

Как уже упоминалось, требования выполняются классом CodeAccessSecurityHelper, частичная реализация которого показана на рис. 7. Все типы полномочий в .NET Framework поддерживают интерфейс IPermission:

Рис. 7 Реализация CodeAccessSecurityHelper (Частичная)

public static class CodeAccessSecurityHelper
{
  public static void DemandClientPermissions<T>(this ClientBase<T> proxy, 
   string operationName) where T : class
  {
   DemandClientConnectionPermissions(proxy.Endpoint);
   DemandTransactionPermissions(proxy.Endpoint,operationName);
   DemandTracingPermissions();
   DemandClientSecurityPermissions(proxy);
   DemandEnvironmentPermissions(proxy);
   DemandClientStorePermissions(proxy.Endpoint);
  }

  internal static void DemandClientConnectionPermissions(
   ServiceEndpoint endpoint)
  {
   PermissionSet connectionSet = new PermissionSet(PermissionState.None);

   if(endpoint.Binding is NetTcpBinding)
   {
    connectionSet.AddPermission(new SocketPermission(
     NetworkAccess.Connect,TransportType.Tcp,
     endpoint.Address.Uri.Host,endpoint.Address.Uri.Port));
      
    connectionSet.AddPermission(new DnsPermission(
     PermissionState.Unrestricted));
   }

   /* Rest of the bindings */ 

   connectionSet.Demand();
  }

  internal static void DemandTransactionPermissions(
   ServiceEndpoint endpoint)
  {
   DemandTransactionPermissions(endpoint,null);
  }

  internal static void DemandTransactionPermissions(
   ServiceEndpoint endpoint, string operationName)
  {
   bool transactionFlow = false;
   bool flowOptionAllowed = false;

   if(endpoint.Binding is NetTcpBinding)
   {
    NetTcpBinding tcpBinding = endpoint.Binding as NetTcpBinding;
    transactionFlow = tcpBinding.TransactionFlow;
   }

   /* Checking other bindings */ 

   if(transactionFlow)
   {
    if(Transaction.Current != null)
    {
     //If operationName is null then at least one operation 
     //needs to allow flow to trigger demand
     foreach(OperationDescription operation in 
      endpoint.Contract.Operations)
     {
      string name = operationName ?? operation.Name;
      if(name != operation.Name)
      {
       continue;
      }
      foreach(IOperationBehavior behavior in operation.Behaviors)
      {
       if(behavior is TransactionFlowAttribute)
       {
        TransactionFlowAttribute attribute = 
         behavior as TransactionFlowAttribute;
        if(attribute.Transactions != 
         TransactionFlowOption.NotAllowed)
        {
         flowOptionAllowed = true;
         break;
        }
       }
      }
      if(flowOptionAllowed)
      {
       break;
      }
     }
     if(flowOptionAllowed)
     {
      IPermission distributedTransactionPermission = 
       new DistributedTransactionPermission(
       PermissionState.Unrestricted);
      distributedTransactionPermission.Demand();
     }
    }
   }
  }
  //Rest of the implementation
}

public interface IPermission : ...
{
  void Demand();
  //More members 
}

Требование любого полномочия выполняется путем создания экземпляра объекта полномочия и вызова его реализации метода Demand («Потребовать»). При создании набора полномочий можно добавить к нему отдельные полномочия, после чего вызвать Demand на наборе полномочий, чтобы потребовать все полномочия в нем.

Метод расширения DemandClientPermissions, принадлежащий CodeAccessSecurityHelper, выполняет основную часть требований от имени PartialTrustClientBase<T> (использование расширения позволяет использовать его с любым классом прокси). У него имеется набор вспомогательных методов, каждый из которых требует полномочий для своего аспекта. На рис. 7 показан код DemandClientConnectionPermissions, используемый для требования полномочии на подключение в соответствии с привязкой. Он изучает тип привязки, использованный прокси, и добавляет соответствующие полномочия к объекту набора полномочий. После этого он вызывает Demand на наборе полномочий.

Метод DemandTransactionPermissions сперва проверяет, использует ли прокси привязку, способную создать поток транзакции. Если это так, он проверяет наличие у вызывающего клиента транзакции для распространения (значение Transaction.Current отлично от null). Если это так, он проверяет коллекцию операций на предмет контракта конечной точки, ищущего вызванную в настоящий момент операцию. Когда операция найдена, DemandTransactionPermissions извлекает коллекцию поведений операции для данной операции. DemandTransactionPermissions исследует каждое поведение, выполняя поиск TransactionFlowAttribute. Если атрибут настроен на разрешение распространения транзакции, то DemandTransactionPermissions требует полномочия распределенной транзакции. Подобным же образом DemandClientPermissions использует другие вспомогательные методы, чтобы требовать соответствующие полномочия.

Наконец, чтобы включить частично доверенные клиенты и обратные вызовы, я определил класс PartialTrustDuplexClientBase<T,C> и применил его почти так же, как PartialTrustClientBase<T>, за исключением того, что он добавляет поддержку дуплекса для клиентов, получающих обратные вызовы.

На этом заканчивается краткий рассказ о разграничении доступа кода WCF вместе с моим решением по использованию частично доверенных клиентов с соответствующими уровнями полномочий на уровне кода В следующем выпуске рубрики я покажу частично доверенные службы и частично доверенные узлы. В него также будут включены некоторые профессиональные методики программирования для WCF и .NET Framework.


CAS на стороне размещения в .NET Framework 3.5

В среде .NET Framework 3.5 WCF разрешает размещать службу посредством привязок BasicHttpBinding, WSHttpBinding и WebHttpBinding только частично доверенному коду и только безо всякой безопасности или с безопасностью передачи данных. Более того, в случае WSHttp­Binding не разрешаются такие аспекты, как безопасность сообщений, надежный обмен сообщениями и транзакции. Все привязки со включенным частичным доверием должны использовать текстовое кодирование. Служба, выполняющаяся в условиях частичного доверия, не может использовать дополнительные средства, такие как диагностика и счетчики производительности.

Принудительное применение ограниченного набора поддерживаемых функций является обязанностью привязок. Каждая привязка, кроме привязок HTTP, активно требует полного доверия от размещения службы. Разрешенные привязки HTTP не требуют полного доверия; вместо этого они требуют полномочий в соответствии с контекстом использования, как показано на рис. 1.

Рис. 1 Требования разрешенных привязок HTTP на размещении

СлучайПолномочия
Basic, веб- и WS- безо всякой безопасности или с безопасностью транспортаПолномочие безопасности с исполнением и инфраструктурой; веб-полномочие для приема вызовов к идентификатору URI.
Вышеупомянутые привязки с внутренним типом службы в отдельной сборке Неограниченное отражение.
Вышеупомянутые привязки с проверенными на подлинность вызовами Полномочие безопасности на управление участником.
 

Все разрешенные привязки HTTP требуют полномочия на выполнение и полномочия на изменение инфраструктуры (полномочие безопасности с флагом инфраструктуры), а также полномочие на прием вызовов на идентификаторе URI, настроенном на их конечную точку (веб-полномочие с флагом приема для идентификатора URI). При проверке подлинности вызовов разрешенные привязки HTTP требуют также полномочия на управление участником потока (полномочие безопасности с флагом управления участником).

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

Существуют дополнительные ограничения настройки, которые необходимо учитывать. Например, файл .config не может содержать никаких ссылок на какие-либо хранилища сертификатов (для серверных сертификатов), поскольку контакт с хранилищем сертификатов заставит WCF потребовать полного доверия. Администраторы должны настраивать эти сертификаты раздельно, используя такие средства, как HttpConfig.exe.

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

В идеале хорошо было бы задействовать все возможности WCF, начиная от распределенных транзакций до надежных вызовов, различных типов учетных данных безопасности, связи приложений в интрасети (или даже на одном компьютере) через TCP, а также каналов межпроцессного взаимодействия (IPC), таких как именованные каналы, не говоря уже о дуплексных обратных вызовах, асинхронных вызовах, диагностике и трассировке, инструментировании и, безусловно, очереди вызовов посредством Microsoft Message Queue (MSMQ). Хотелось бы добиться всего этого, не жертвуя CAS, то есть не прибегая к полному доверию.


Частично доверенные службы

В .NET Framework 3.0 единственный способ выполнения службы на условиях частичного доверия состоял в явном разрешении пользоваться только полномочиями, необходимыми службе для работы и, следовательно, неявном отказе во всех других полномочиях. Одним из путей достижения этого являлось применение атрибутов полномочий с флагом SecurityAction.PermitOnly. Рассмотрим службу в следующей ситуации.

[SecurityPermission(
  SecurityAction.PermitOnly,
  Execution = true)]
[UIPermission(SecurityAction.PermitOnly,
  Window =
   UIPermissionWindow.SafeTopLevelWindows)]
class MyService : IMyContract {
  public void MyMethod() {
   Form form = new TestForm();
   form.ShowDialog();
  }
}

Службе требуется полномочие для ее выполнения (как в случае любого управляемого кода), а кроме этого ей необходимо полномочие для отображения безопасных окон. Если бы требовалось скомпоновать для класса несколько атрибутов полномочий типа «только разрешение», это привело бы к единственному набору полномочий на этапе выполнения.

В .NET Framework такой подход используется для разрешения только этих полномочий и для активного отклонения всех других полномочий посредством установки специального модификатора анализа стека. В результате, даже если сборка, в которой находится служба (как и область приложений), предоставляет службе полное доверие, во всех других полномочиях службе будет откaзано.

Когда служба пытается выполнить некую операцию, например открытие файла, создается исключение безопасности, поскольку запрос на операцию ввода или вывода файла сталкивается со модификатором анализа стека, который активно отказывает в этом полномочии. Все, что остается делать службе, это выполняться в своей виртуальной «песочнице» и отображать безопасные окна. .NET Framework изменит заголовок окна и выведет предупреждающий тег в отображаемую форму, чтобы проинформировать пользователя о том, что приложение является частично доверенным.

Вместо указания полномочий в качестве атрибутов их можно перечислить в файле XML, содержащем набор полномочий, и предоставить имя этого файла методу PermissionSetAttribute, как показано на рис. 3.

Рис. 3 Использование файла с набором полномочий для частично доверенной службы

<!-- MyServicePermissions.xml -->
<PermissionSet class = "System.Security.PermissionSet">
  <IPermission
   class = "System.Security.Permissions.SecurityPermission"
   Flags = "Execution"
  />
  <IPermission
   class = "System.Security.Permissions.UIPermission"
   Window = "SafeTopLevelWindows"
  />
</PermissionSet>

[PermissionSet(SecurityAction.PermitOnly,File =
  "MyServicePermissions.xml")]
class MyService : IMyContract {
  public void MyMethod() {
   Form form = new TestForm();
   form.ShowDialog();
  }
}

Обратите внимание, что файл с набором полномочий используется только на этапе компиляции и бесполезен при развертывании. Компилятор внедрит разрешенные полномочия в класс метаданных в качестве отдельных атрибутов. Если файл отсутствует, сборка закончится аварийно.


Размещение области приложений

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

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

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

Для размещения, а также для собственной защиты службы было бы предпочтительнее разрешить размещению предоставлять любые полномочия, которые оно считает подходящими для службы. С этой целью я написал класс AppDomainHost, определенный на рис. 4.

Рис. 4 AppDomainHost

public class AppDomainHost : IDisposable {
  //Create new app domain in full trust
  public AppDomainHost(Type serviceType,params Uri[] baseAddresses);
  public AppDomainHost(Type serviceType,string appDomainName,
   params Uri[] baseAddresses);

  //Create new app domain with specified permission set
  public AppDomainHost(Type serviceType,PermissionSet permissions,
   params Uri[] baseAddresses);
  public AppDomainHost(Type serviceType,PermissionSet permissions,
   string appDomainName,params Uri[] baseAddresses);

  //Additional constructors that take standard permission set,
  //permission set filename, and an existing app domain  

  public void Open();
  public void Close();
  public void Abort();

  //More members
}

Класс AppDomainHost можно использовать точно так же, как классc ServiceHost, предоставляемый платформой WCF:

AppDomainHost host = 
  new AppDomainHost(typeof(MyService));
host.Open();

Отличие состоит в том, что класс AppDomainHost будет размещать службу предоставляемого типа в новой области приложения, а не в области вызывающей ее стороны. По умолчанию имя новой области приложений будет состоять из «AppDomain Host for» с добавлением суффикса в виде типа службы и нового идентификатора GUID. Предусмотрена также возможность указывать имя новой области приложений.

AppDomainHost host = 
  new AppDomainHost(typeof(MyService),"My App Domain");
host.Open();

По умолчанию новая область приложений создается с полным доверием. Однако помещение службы в отдельную область приложений является ключом к управлению частично доверенными службами.
AppDomainHost позволяет предоставлять полномочия новой области приложений. Например, на рис. 5 показана служба и размещение, предоставляющее службе достаточно полномочий, необходимых для ее работы.

Рис. 5 Частично доверенная служба

class MyService : IMyContract {
  public void MyMethod() {
   Form form = new TestForm();
   form.ShowDialog();
  }
}

//Hosting code:
PermissionSet permissions =
  new PermissionSet(PermissionState.None);
permissions.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));
permissions.AddPermission(new UIPermission(
  UIPermissionWindow.SafeTopLevelWindows));

AppDomainHost host =
  new AppDomainHost(typeof(MyService),permissions);

host.Open();

Можно также использовать файл с набором полномочий, и, в отличие от того, что показано на рис. 3, этот файл необходим только на этапе выполнения, и его можно изменить после развертывания. Важно отметить, что имеется также возможность указывать один из стандартных именованных наборов полномочий. Виртуальная «песочница», предписанная частично доверенным службам, оказывает влияние на все подчиненные классы, которые могут использоваться наряду с возможностью делать вызов извне «песочницы» посредством любой технологии, от .NET Framework 1.0 до WCF.

Единственный размещающий процесс может иметь много экземпляров класса AppDomainHost, каждый из которых размещает службы того же или другого типа, имеющие любые произвольные полномочия, как показано на рис. 6 (используется такое же определение службы, как на рис. 5).

Рис. 6 Служба того же типа с другими полномочиями

//Default is service with full trust
AppDomainHost host0 =
  new AppDomainHost(typeof(MyService),
  "Full Trust App Domain",
  new Uri("net.tcp://localhost:6000"));

host0.Open();

//With just enough permissions to do work
PermissionSet permissions1 =
  new PermissionSet(PermissionState.None);
permissions1.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));
permissions1.AddPermission(new UIPermission(
  UIPermissionWindow.SafeTopLevelWindows));

AppDomainHost host1 =
  new AppDomainHost(typeof(MyService),permissions1,
  "Partial Trust App Domain",
  new Uri("net.tcp://localhost:6001"));

host1.Open();

//With not enough permissions to do work
PermissionSet permissions2 =
  new PermissionSet(PermissionState.None);
permissions2.AddPermission(new SecurityPermission(
  SecurityPermissionFlag.Execution));

AppDomainHost host2 =
  new AppDomainHost(typeof(MyService),permissions2,
  "Not enough permissions",new Uri("net.tcp://localhost:6002"));

host2.Open();

//Using one of the named permission sets
AppDomainHost host3 =
  new AppDomainHost(typeof(MyService),
  StandardPermissionSet.Internet,
  "Named permission set",
  new Uri("net.tcp://localhost:6003"));

host3.Open();


Реализация класса AppDomainHost

Реализация класса выполняется в два приема: создание новой области приложений и внедрение экземпляра размещения службы, затем последующее выполнение размещения службы (и экземпляра службы) в условиях частичного доверия. Для создания экземпляра размещения службы и активирования его в отдельной области приложения я написал класс ServiceHostActivator, показанный на рис. 7.

Рис. 7 Класс ServiceHostActivator

class ServiceHostActivator : MarshalByRefObject {
  ServiceHost m_Host;

  public void CreateHost(Type serviceType,Uri[] baseAddresses) {
   m_Host = new ServiceHost(serviceType,baseAddresses);
  }
  public void Open() {
   m_Host.Open();
  }  
  public void Close() {
   m_Host.Close();
  }
  public void Abort() {
   m_Host.Abort();
  }

  //Rest of the implementation
}

Класс ServiceHostActivator представляет собой простую обертку стандартного экземпляра класса ServiceHost, предоставляемого платформой WCF. ServiceHostActivator порождается от MarshalByRefObject таким образом, чтобы класс AppDomainHost мог вызывать его через границу области приложений. В методе CreateHost инкапсулировано конструирование нового экземпляра ServiceHost. Остальные методы класса Service­HostActivator просто пересылают удаленные вызовы базовому экземпляру размещения.
AppDomainHost предлагает несколько перегружаемых конструкторов. Эти конструкторы могут вызывать друг друга (см. рис. 8), даже создавая по пути новую область приложений, и в конечном итоге конструирование завершается использованием защищенного конструктора, принимающего тип службы, экземпляр новой области приложений, полномочия для новой области и базовые адреса.

Рис. 8. Реализация класса AppDomainHost

public class AppDomainHost : IDisposable {
  ServiceHostActivator m_ServiceHostActivator;

  public AppDomainHost(Type serviceType,
   params Uri[] baseAddresses) :  
   this(serviceType,"AppDomain Host for "+
   serviceType+" "+Guid.NewGuid(),
   baseAddresses) {
  }

  public AppDomainHost(Type serviceType,
   string appDomainName,
   params Uri[] baseAddresses) : this(serviceType,
   new PermissionSet(PermissionState.Unrestricted),
   appDomainName,baseAddresses) {
  }

  public AppDomainHost(Type serviceType,
  PermissionSet permissions,
  string appDomainName,
  params Uri[] baseAddresses) :
  this(serviceType,AppDomain.CreateDomain(appDomainName),
  permissions,baseAddresses) {
  }

  //More constructors

  protected AppDomainHost(Type serviceType,
   AppDomain appDomain,
   PermissionSet permissions,Uri[] baseAddresses) {

   string assemblyName = Assembly.GetAssembly(
    typeof(ServiceHostActivator)).FullName;
   m_ServiceHostActivator = appDomain.CreateInstanceAndUnwrap(
    assemblyName,typeof(ServiceHostActivator).ToString()) as
    ServiceHostActivator;

   CodeAccessSecurityHelper.SetPermissionsSet(appDomain,permissions);

   m_ServiceHostActivator.CreateHost(serviceType,baseAddresses);
  }

  public void Open() {
   m_ServiceHostActivator.Open();
  }
  public void Close() {
   m_ServiceHostActivator.Close();
  }
  public void Abort() {
   m_ServiceHostActivator.Abort();
  }
  void IDisposable.Dispose() {
   Close();
  }
}

Защищенный конструктор класса AppDomainHost использует технологию удаленного вызова .NET для внедрения в новую область приложений экземпляра класса ServiceHostActivator, завершая предоставлением ему удаленного прокси, хранящегося в члене m_ServiceHostActivator.

По умолчанию новая область приложений создается с полным доверием. Класс AppDomainHost использует метод SetPermissionsSet моего класса CodeAccessSecurityHelper для установки в новой области приложений новой политики CAS. Эта политика разрешает только предлагаемые полномочия и отказывает в остальных.

public static class CodeAccessSecurityHelper {
  public static void SetPermissionsSet(
   AppDomain appDomain,
   PermissionSet permissions) {

   PolicyLevel policy = PolicyLevel.CreateAppDomainLevel();
   policy.RootCodeGroup.PolicyStatement =
    new PolicyStatement(permissions);
   appDomain.SetAppDomainPolicy(policy);
  }
  //More members
}

Это так же просто, как создание новой политики безопасности на уровне области приложений и вызов метода SetAppDomainPolicy класса AppDomain.

public sealed class AppDomain :
  MarshalByRefObject,... {

  public void SetAppDomainPolicy(
   PolicyLevel domainPolicy);
  //More members
}

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

При вызове других методов класса AppDomainHost, таких как Open или Close, используется прокси класса ServiceHostActivator с целью вызова другой области приложений и обеспечения открытия или закрытия им его экземпляра размещения. Поскольку экземпляр службы будет выполняться в области приложений, которой случилось открыть его размещение, служба также будет выполняться согласно политике безопасности этой области приложений.


Частично доверенные размещения

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

А что, если запускаемый код является только частично доверенным? Можно было бы поместить AppDomainHost и ServiceHostActivator в сборку, обладающую полным доверием, разрешить частично доверенные вызывающие и предоставить им обоим полное доверие.

[PermissionSet(SecurityAction.Assert,Unrestricted = true)]
public class AppDomainHost : IDisposable
{...}

[PermissionSet(SecurityAction.Assert,Unrestricted = true)]
class ServiceHostActivator : MarshalByRefObject
{...}

Хотя этот подход дает результат, он обходит CAS и отключает жизненно важный механизм безопасности, что приводит к возникновению двух проблем безопасности. Во-первых, неправильно полагать, что код, открывающий размещение, обладает полномочиями на прием вызовов из транспортных каналов или на участие в каких-либо действиях WCF, таких как распределенные транзакции. Во-вторых, декларируя полное доверие и подавляя анализ стека, частично доверенный код, запустивший размещение, может на деле быть использован для создания службы с более высокими правами, чтобы она выполнила за него грязную работу. Например, размещающий код может не обладать полномочиями на ввод и вывод файлов, но он использует класс AppDomainHost для принятия вызовов к службе, имеющей полномочия на ввод и вывод файлов.

Решение является простым: классы AppDomainHost и ServiceHostActivator не должны использовать пустое утверждение полного доверия. Вместо этого, когда это необходимо, они должны его утверждать только локально, а затем возвращаться к существующим полномочиям. Кроме этого, класс AppDomainHost должен испытывать код, используя его, и запрашивать соответствующие полномочия размещения (например, принятие вызовов). Также он должен проверять факт предоставления размещающему коду по крайней мере тех полномочий, которые ему требуются для выполнения службы. Это предотвратит использование ограниченным и частично доверенным кодом службы, размещенной в отдельной области приложений и имеющей более широкие полномочия. В итоге мы получаем структурированные требования безопасности на стороне размещения.


Структурированные требования безопасности на стороне размещения

Переработанный класс ServiceHostActivator, пригодный для частично доверенных источников вызовов, показан на рис. 9. Метод CreateHost класса ServiceHostActivator не может декларативно заявлять полное доверие, поскольку это помешает ему запрашивать соответствующее полномочие вызывающего кода на размещение. Вместо этого он программным образом использует класс CodeAccessSecurityHelper для создания набора полномочий полного доверия и подтверждает его, затем переходит к созданию размещения. Когда WCF запрашивает полное доверие, утверждение препятствует подъему этого запроса в стеке вызовов.

Рис. 9 Переработанный класс ServiceHostActivator

class ServiceHostActivator : MarshalByRefObject {
  public void CreateHost(Type serviceType,Uri[] baseAddresses) {
   CodeAccessSecurityHelper.PermissionSetFromStandardSet(
   StandardPermissionSet.FullTrust).Assert();

   m_Host = new ServiceHost(serviceType,baseAddresses);

   PermissionSet.RevertAssert();

   m_Host.DemandHostPermissions();
  }

  //Behavior demands happen here, must assert
  [PermissionSet(SecurityAction.Assert,Unrestricted = true)]
  public void Open() {
   m_Host.Open();
  }  
  ...
}

После создания размещения CreateHost явно изменяет подтверждение на противоположное. Затем CreateHost вызывает вспомогательный метод расширения DemandHostPermissions класса CodeAccessSecurityHelper из только что созданного экземпляра размещающего класса, чтобы запросить полномочия на размещение. DemandHostPermissions исследует объект размещения и запрашивает соответствующие полномочия.

Например, если размещение поддерживает конечную точку TCP, тогда DemandHostPermissions запрашивает полномочие на выполнение и полномочие на принятие вызовов TCP на идентификаторе URI конечной точки. Если надежный обмен сообщениями используется посредством TCP, то DemandHostPermissions запрашивает полномочие на управление политикой. Существует немало дополнительных возможных запросов, как показано на рис. 10.

Рис. 10 Запросы на стороне размещения, структурированные посредством AppDomainHost

СлучайПолномочия
TCPПолномочие безопасности с исполнением, полномочие сокета на принятие вызова TCP на идентификаторе URI
IPCПолномочие безопасности с исполнением, управлением политикой и доказательствами
MSMQПолномочие безопасности с исполнением, полномочие MSMQ на чтение из очереди
WS, WS-Dual, Basic, WebПолномочие безопасности с исполнением и инфраструктурой и веб-полномочие для приема вызовов на идентификаторе URI
RM через TCPПолномочие безопасности для управления политикой
Проверенные на подлинность вызовыПолномочие безопасности для управления участником
Распространение транзакцийНеограниченное полномочие распределенных транзакций
Учетные данные имени пользователя с безопасностью сообщений, учетные данные сертификатов с проверкой и сертификат службыПолномочие хранилища на перечисление хранилищ, открытие хранилища и перечисление сертификатов
Диагностическое отслеживаниеПолномочие среды на чтение COMPUTERNAME, полномочие ввода и вывода файлов на обнаружение пути, добавление и запись в файлы журнала
Счетчики производительности службы WCFПолномочие счетчика производительности на запись в счетчик службы и в счетчик операции
Счетчики общей производительности WCFПолномочие счетчика производительности на запись в счетчик службы, запись в счетчик конечной точки, запись в счетчик операции и запись в счетчик размещения
Поставщики ASP.NETМинимальное полномочие на размещение ASP.NET

Использование привязки IPC требует полномочия на управление политикой и доказательствами. Использование привязки MSMQ инициирует запрос полномочия на чтение из очереди конечной точки. Использование любой из привязок HTTP инициирует запрос на принятие вызовов на адресе конечной точки. Если привязка использует проверенные на подлинность вызовы, то DemandHostPermissions запрашивает полномочия на управление участником.

Использование привязки с поддержкой транзакции, активированным потоком транзакций и по крайней мере одной операцией из контракта конечной точки с разрешенным (или обязательным) потоком транзакций приводит к запросу неограниченного полномочия распределенных транзакций.

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

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

Если для проверки подлинности источника вызова или членства в роли размещение опирается на поставщики ASP.NET, класс DemandHostPermissions запрашивает минимальные полномочия размещения ASP.NET (такие полномочия требуются всем поставщикам). При выполнении обратного вызова посредством привязки WS-Dual сама привязка запрашивает веб-полномочие для подключения к данной конечной точке обратного вызова, поэтому у класса AppDomainHost нет необходимости явно запрашивать его при запуске размещения.


Реализация структурированных требований размещения

На рис. 11 показана частичная реализация метода DemandHostPermissions, в которой сначала выполняются требования, определяемые конечной точкой. Осуществляется перебор коллекции конечных точек службы, и для каждой конечной точки выставляется требование соответствующих полномочий на подключение, безопасность и транзакции.

Рис. 11 Реализация метода DemandHostPermissions

public static class CodeAccessSecurityHelper {
  internal static void DemandHostPermissions(this ServiceHost host) {
   foreach(ServiceEndpoint endpoint in host.Description.Endpoints) {
    DemandHostConnectionPermissions(endpoint);
    DemandHostSecurityPermissions(endpoint);
    using(TransactionScope scope = new TransactionScope()) {
     DemandTransactionPermissions(endpoint);
    }
   }
   DemandHostStorePermissions(host);
   DemandPerformanceCounterPermissions();
   DemandTracingPermissions();
   DemanAspNetProvidersPermissions(host);
  }
  internal static void DemandHostConnectionPermissions(
   ServiceEndpoint endpoint) {

   PermissionSet connectionSet =
    new PermissionSet(PermissionState.None);
   if(endpoint.Binding is NetTcpBinding) {
    connectionSet.AddPermission(new SocketPermission(
     NetworkAccess.Accept, TransportType.Tcp,
     endpoint.Address.Uri.Host,endpoint.Address.Uri.Port));
   }

   /* Checking the other bindings */

   connectionSet.Demand();
  }

  static void DemanAspNetProvidersPermissions(ServiceHost host) {
   bool demand = false;

   foreach(IServiceBehavior behavior in host.Description.Behaviors) {
    if(behavior is ServiceCredentials) {
     ServiceCredentials credentialsBehavior =
      behavior as ServiceCredentials;

     if(credentialsBehavior.UserNameAuthentication.
      UserNamePasswordValidationMode ==
      UserNamePasswordValidationMode.MembershipProvider) {

      demand = true;
      break;
     }
    }
    if(behavior is ServiceAuthorizationBehavior) {
     ServiceAuthorizationBehavior serviceAuthorization =
      behavior as ServiceAuthorizationBehavior;
     if(serviceAuthorization.PrincipalPermissionMode ==
      PrincipalPermissionMode.UseAspNetRoles &&
      Roles.Enabled) {

      demand = true;
      break;
     }
    }
   }
   if(demand) {
    IPermission permission =
     new AspNetHostingPermission(
     AspNetHostingPermissionLevel.Minimal);
    permission.Demand();
   }
  }
  //Rest of the implementation
}

Для запроса транзакций создается временная внешняя транзакция с помощью области действия транзакции. Это позволяет использовать тот же метод, что и на клиентской стороне, который запрашивает полномочия в момент вызова на основании наличия внешней транзакции.

После запросов конечных точек метод DemandHostPermissions запрашивает полномочия, являющиеся продуктом настройки размещения службы, а именно полномочие на доступ к хранилищу, полномочия счетчиков производительности и полномочия поставщиков ASP.NET. Например, для запроса полномочий поставщиков ASP.NET метод получает коллекцию поведений службы и проверяет, использует ли поведение размещения в отношении учетных данных службы поставщика членства, или проверяет, использует ли поведение размещения в отношении авторизации службы поставщиков ролей, и если использует, запрашивает полномочие размещения ASP.NET.

Класс AppDomainHost также требует некоторой переработки для поддержки структурированных требований в частично доверенной среде, как показано далее.

[SecurityPermission(
  SecurityAction.Assert,ControlAppDomain = true)]
[ReflectionPermission(
  SecurityAction.Assert,Unrestricted = true)]
public class AppDomainHost : IDisposable {
  protected AppDomainHost(Type serviceType,
   AppDomain appDomain,
   PermissionSet permissions,
   params Uri[] baseAddresses) {
   //Cannot grant service permissions
   // the host does not have
   permissions.Demand();

   //Rest of constructor
   }
}

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


Дополнительные аспекты класса AppDomainHost

Хотя класс AppDomainHost не поддерживает ICommunication­Object, он предлагает свою модель событий и управление конечным автоматом. AppDomainHost копирует также некоторые статические переменные из вызывающей области приложений во вновь созданную им область приложений. А именно, он использует ServiceHostActivator для копирования имени приложения поставщиков ASP.NET в другую область приложений. Класс AppDomainHost временно подтверждает полномочие размещения ASP.NET, получает доступ к поставщикам, копирует значения членства и имен ролей приложения и меняет подтверждение на противоположное. 

Скачать исходники примеров 

Джувел Лоуи (Juval Lowy) — архитектор программного обеспечения в компании IDesign, проводящий обучение по WCF и дающий консультации по архитектуре WCF. Он также является региональным директором Майкрософт в Силиконовой долине. Он недавно выпустил книгу Programming WCF Services («Программирование служб WCF»).