In .NET 3.0, the only way for a service to execute in partial trust was to explicitly
permit only the permissions required for it to operate, and implicitly deny all other
permissions. One way of achieving that is to apply the matching permission attributes with
the SecurityAction.PermitOnly
flag. Consider the service
in Example D-7.
Example D-7. Using permission attributes for a partially trusted service
[SecurityPermission(SecurityAction.PermitOnly,Execution = true)] [UIPermission(SecurityAction.PermitOnly, Window = UIPermissionWindow.SafeTopLevelWindows)] class MyService : IMyContract { public void MyMethod( ) { Form form = new TestForm( ); form.ShowDialog( ); } }
The service requires permission to execute (as does all managed code) and permission to display safe windows. Stacking multiple permit-only permission attributes on a class yields at runtime a single permission set that .NET uses to allow only those permissions and actively deny all others, by installing a dedicated stack-walk modifier. Even if the assembly the service resides in (as well as the app domain) grants the service full trust, all other permissions will be denied. For instance, if the service tries to perform an operation such as opening a file, that will trigger a security exception because the demand for file I/O will encounter the stack-walk modifier, which will actively deny having that permission. All the service can do is execute in its virtual sandbox and display safe windows. Not only that, but .NET will change the window caption and display a warning tag on the displayed form to inform the user that the application is partially trusted, as seen in Figure D-1.
Instead of specifying the permissions as attributes, you can list them in a permission
set XML file and provide that file's name to the PermissionSetAttribute
, as shown in Example D-8.
Example D-8. Using a permission set file for a partially trusted service
<!-- 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( ); } }
Note that the permission set file is used only at compile time, not during deployment. The compiler will fuse the permitted permissions into the class metadata, as with individual attributes. If the file is not present, the build will fail.
There are several problems with the attribute-based approach to partially trusted services. First, every time there is a change in what the service does—or, for that matter, in any of the downstream classes it uses—you will need to modify the stack of attributes or the permission set file. The service is therefore coupled to those downstream classes, and the maintenance cost is increased.
Second, because the permissions are part of the service definition, there is no way to use the service with different permissions as a product of the hosting environment. That is, you cannot dynamically give it more or fewer permissions.
Third, and most important, it is unlikely that most services will actually go to the
trouble of analyzing the security permissions required for them to operate and
meticulously reducing their granted permissions in order to run with the least possible
privileges and permissions. If anything, it is the host that should be concerned about
what exactly the service it loads is up to, and yet the host by default has no ability to
affect the service's permissions. WCF's ServiceHost
hosts all services in full trust. It would be better for the host, as well as for the
service's own protection, to allow the host to grant the service whichever permissions it
deems appropriate.
To that end, I wrote the class AppDomainHost
,
defined in Example D-9.
Example D-9. The class 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); public AppDomainHost(Type serviceType,StandardPermissionSet standardSet, 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
can be used just like the
WCF-provided ServiceHost
:
AppDomainHost host = new AppDomainHost(typeof(MyService)); host.Open( );
The difference is that AppDomainHost
will host the
provided service type in a new app domain, not in the app domain of its caller. The new
app domain name will default to "AppDomain Host for", suffixed with the service type and a
new GUID. You can also specify a name for the new app domain:
AppDomainHost host = new AppDomainHost(typeof(MyService),"MyService App Domain"); host.Open( );
The new app domain is created with full trust by default. However, placing the service
in a separate app domain is the key to managing partially trusted services. AppDomainHost
allows you to provide the permissions for the
new app domain. For example, to host this service:
class MyService : IMyContract { public void MyMethod( ) { Form form = new TestForm( ); form.ShowDialog( ); } }
with the same permissions as in Example D-7, you would write:
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( );
You can also use a permission set file, but unlike with Example D-8, the file is needed only at runtime and can be modified post-deployment. You can also specify one of the standard named permission sets.
A single host process can have as many instances of AppDomainHost
as required, each hosting the same or a different service type
and each with any arbitrary set of permissions, as shown in Example D-10.
Example D-10. Hosting the same service type with different permissions
//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, "My 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( );
There are two parts to implementing AppDomainHost
: creating a new app domain and injecting a service host
instance into it, and then having the service host (and the service instance) execute in
partial trust. To create a service host instance and to activate it in a separate app
domain, I wrote the class ServiceHostActivator
, shown
in Example D-11.
Example D-11. The ServiceHostActivator class (partial)
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
is a simple wrapper around a
standard WCF-provided ServiceHost
instance. ServiceHostActivator
derives from MarshalByRefObject
, so AppDomainHost
can
call it across the app domain boundary. The CreateHost(
)
method encapsulates constructing a new ServiceHost
instance. The rest of the methods of ServiceHostActivator
just forward the remote calls to the underlying host
instance. AppDomainHost
offers several overloaded
constructors. These constructors all call each other (see Example D-12), even creating a new app domain along
the way. Eventually, the public constructors end up using a protected constructor that
takes the service type, the new app domain instance, the permission set for the new
domain, and the base addresses.
Example D-12. Implementing AppDomainHost (partial)
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 constructorsprotected 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; appDomain.SetPermissionsSet(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( ); } }
The protected constructor of AppDomainHost
uses
.NET Remoting to inject into the new app domain an instance of ServiceHostActivator
, ending up with a Remoting proxy to it stored in the
m_ServiceHostActivator
member.
The new app domain is created with full trust by default. AppDomainHost
uses the SetPermissionsSet( )
AppDomain
extension method of CodeAccessSecurityHelper
to install in the new app domain a new code-access
security policy, permitting only the supplied permissions and denying the rest. Example D-13 shows the implementation of SetPermissionsSet( )
.
Example D-13. Implementing SetPermissionsSet( )
public static class CodeAccessSecurityHelper
{
public static void SetPermissionsSet(this
AppDomain appDomain,
PermissionSet permissions)
{
PolicyLevel policy = PolicyLevel.CreateAppDomainLevel( );
policy.RootCodeGroup.PolicyStatement = new PolicyStatement(permissions);
appDomain.SetAppDomainPolicy(policy);
}
//More members
}
Assigning the permissions to the new app domain is as simple as creating a new
security policy at the app domain level and calling the SetAppDomainPolicy( )
method of the AppDomain
class:
public sealed class AppDomain : MarshalByRefObject,... { public void SetAppDomainPolicy(PolicyLevel domainPolicy); //More members }
A similar technique to Example D-13 is what ClickOnce uses to enforce the partial-trust execution of ClickOnce-deployed applications.
When you call the other methods of AppDomainHost
,
such as Open( )
or Close(
)
, the proxy to ServiceHostActivator
calls to the other app domain and has it there open or close its host instance. Since
the service instance will execute in the app domain that happened to open its host, the
service will also execute under that app domain's security policy.
3.129.194.123