Defining Custom Permissions

Because .NET security is largely implemented by classes in the framework class library, it's possible to define custom permissions if you are implementing some library that needs to be protected against partially trusted callers, but for which the permissions supplied by Microsoft are not appropriate. Admittedly, defining custom permissions is not something that you should need to do very often in the course of normal programming, but I'm going to use this as the basis for the first sample in this chapter, because defining a custom permission provides considerable insight into how .NET security works under the hood, illustrating many of the principles we've been discussing about how classes are instantiated from XML representations.

Since code access permissions are implemented as classes derived from CodeAccessPermission, it follows that defining your own permission normally involves defining your own class, also derived from CodeAccessPermission. There are also other steps to be taken, including adding your custom permissions to the security policy. We'll see all these steps in action as we work through the next sample.

LightDetector Sample

The first sample we will present will illustrate the process of creating a custom permission. For this sample, we will assume that Wrox Press has branched out from publishing, into a new business market, manufacturing light detectors. These devices can plug into computers and supply information about the light they are detecting - something like a camera but much simpler (a better analogy would be those light detectors that come with Lego Mindstorms models, but obviously I wouldn't want to mention such childish activities in a serious computer book...). For our sample, we assume that the Wrox Press light detectors come with device drivers that have a managed interface. Of course, the DLL that controls the device drivers will be installed on the local system, which means it has full trust and therefore isn't very interesting from the point of view of demonstrating security. However, to make things more interesting, we'll assume that Wrox Press has recently expanded the software by making available also some application that can retrieve and somehow process the data the light detectors are seeing. This application is not normally installed on the local machine, but can be downloaded from a web site, which means it is running in a very low-trust environment. This means that we are going to need some custom permissions in order to restrict access to the light detector so that our Wrox application can access the device, but the device can't be accessed from any other untrusted code that we might download. In real life, the usual way to handle that case would be for Wrox Press to sign their software with a digital certificate, and for us to set up another code group, which allows access to the light detector by any software that has been signed with this certificate. However, we haven't covered certificates yet - that's an important topic in its own right which we will examine in Chapter 13, so instead we will set up a code group whose membership condition is for assemblies to have been signed with a certain strong name, for which the key file is supplied with the sample.

For the purposes of our sample, processing the data simply means displaying its value when the user hits the Read Color button. Here's what the app looks like:

click to expand

Notice that the CLR has actually added some information to the title bar to indicate the source of this application. I grabbed the screenshot by placing the sample on my own web site, www.simonrobinson.com, and setting this web site to be in my trusted zone as far as Internet Explorer settings are concerned. We see in the screenshot that the code for the application has placed its own name, LightDetector, in the title bar. The CLR prefixes 'Trusted' to indicate that the application has been downloaded and run from a web site in my trusted zone, and also appends the actual URL (this text unfortunately overflows the title bar area).

The overall architecture of the sample looks like this:

click to expand

In this diagram, the arrows indicate the dependencies of the assemblies. The purposes of the assemblies are:

  • LightDetectorPermission.dll defines the custom security permissions that are needed to control access to the light detector.

  • LightController.dll implements managed classes that can talk to the device drivers and thereby offer a managed interface through which other code can access the light detector.

  • LightDetector.exe is a simple Windows Forms application that displays the value read from the light detector. Because this file sits on a remote machine, we'll need to configure security to allow it to invoke the relevant methods in LightController.dll. As just mentioned, we'll do this by signing LightDetector.exe with a strong name and setting up a code group to allow access to the light detector by any code signed with this strong name.

  • GenerateXmlFiles.exe is a helper assembly that generates the XML representation of the permissions we need to define. This is important because we need the XML representation in order to add the permissions to the local CLR security policy.

Obviously, this sample is simplified relative to any real application. In particular, I'm not providing a real light detector for the sample. The LightController.ReadValue() method, which supposedly returns an RGB value indicating the level of light the controller can see, in our sample just returns the hard coded value of {R=255, G=40, B=30}. Also, in real life the GenerateXmlFiles.exe assembly would probably not be shipped out to clients - it would be used in-house to generate the XML files, which would then presumably be shipped as part of some integrated deployment installation package that installs the files and automatically registers the custom security permissions with the CLR. Since our goal here is to learn about how security works, we'll have the GenerateXmlFiles.exe assembly on the 'client' machine and work through the registration process by hand.

The process of compiling these assemblies is relatively complex: we not only need to compile them, but we must also install the two DLLs in the GAC, and run GenerateXmlFiles.exe to get the XML files. In view of this, the downloadable version of this sample is supplied not as a VS.NET project, but as a set of plain .cs source files (or .vb source files for the VB version), along with a batch file to perform the compilation.

LightDetectorPermission.dll

This file defines the custom permission. The permission is called LightDetectorPermission (the same name as the assembly), and has a couple of levels at which it can be granted, which we will define by the following enumeration:

 [Flags] public enum LightDetectorPermissions {    All = 3,    Read = 2,    Reset = 1,    None = 0 } 

Now for the permission itself. The permission contains one member field, which is used to indicate the level of permission this object represents, as well as two constructors:

 [Serializable()] public sealed class LightDetectorPermission : CodeAccessPermission,                                               IUnrestrictedPermission {    LightDetectorPermissions state;    public LightDetectorPermission(LightDetectorPermissions state)    {       this.state = state;    }    public LightDetectorPermission(PermissionState permState)    {       if (permState == PermissionState.Unrestricted)          this.state =  LightDetectorPemissions.All;       else          this.state = LightDetectorPermissions.None;    } // etc. 

The purpose of the first constructor should be fairly obvious. The second constructor is required to support the CLR's security infrastructure. At certain times, the CLR will internally instantiate the permission class, and will want to be able to indicate whether or not the permission object represents unrestricted access (in other words, complete access to all resources controlled by this permission; in our case this is equivalent to the LightDetectorPermissions.All flag). This concept is represented by the System.Security.PermissionState enumeration, which has just two values, Unrestricted and None. The second constructor in the above code simply takes a PermissionState and uses it to set an appropriate permission level.

Next we need to implement a few other methods that are required to support the .NET security infrastructure. These methods are all declared as abstract in the base class, CodeAccessPermission. First a method that tells the CLR whether this permission object gives completely unrestricted access to its resource. This method is used on occasions by the security infrastructure:

 public bool IsUnrestricted() {    return state == LightDetectorPermissions.All; } 

Next we have a method that can return a copy of this permission object, cast to the IPermission interface:

 public override IPermission Copy() {    return new LightDetectorPermission(this.state); } 

We also need a method that can deal with the situation in which some code demands two successive LightDetectorPermissions, which must both be satisfied for the code to run. In this case, the CLR must be able to supply a permission object that represents exactly the set of conditions needed to satisfy both permissions (the intersection of the permissions): the CLR does this by calling an override method IPermission.Intersect() on our permission object:

 public override IPermission Intersect(IPermission target) {    LightDetectorPermission rhs = target as LightDetectorPermission;    if (rhs == null)       return null;    if ((this.state & rhs.state) == LightDetectorPermissions.None)       return null;    return new LightDetectorPermission(this.state & rhs.state); } 

Because the possible permission state in our class is represented by a [Flags] enumeration, we can fairly simply identify the intersection permission by performing a bitwise AND operation on the two state masks. Note that this method should only ever be called with a LightDetectorPermission reference passed in as a parameter, but just in case, we start off by checking that that is the case - and if it isn't, we return null, indicating that any intersection contains no permissions. We also return null if we figure out the intersection of the two permissions results in no permission to do anything.

Another related task is that the CLR will sometimes need to know if our permission represents a subset of another permission. Here's our method to deal with this task:

 public override bool IsSubsetOf(IPermission target) {    if (target == null || !(target is LightDetectorPermission))       return false;    LightDetectorPermission rhs = (LightDetectorPermission)target;    int subsetFlags = (int)this.state;    int supersetFlags = (int)rhs.state;    return (subsetFlags & (~supersetFlags)) == 0; } 

Again, our bitmask representation of the access flags makes this method relatively easy to implement. If the permissions in this object really do form a subset of those of the target object, then there will be no bits in our mask which are one (representing a permission granted) in the this object and zero (representing a permission denied) in the target object. The bitwise operation subsetFlags & (~supersetFlags) will return zero if that's the case. Obviously, if we had a more complicated permission - such as one that could be narrowed down to individual files, then our implementations of IsSubsetOf() and Intersect() would be considerably more complex.

The Copy(), Intersect(), and IsSubsetOf() methods are defined in the IPermission interface, and are not implemented by the CodeAccessPermission class - which gives a syntactical requirement for our class to implement these methods.

Next we need to implement the methods defined in ISecurityEncodable which allow conversion to and from an XML representation of this permission:

 public override void FromXml(SecurityElement xml) {    string element = xml.Attribute("Unrestricted");    if(element != null)    {       state = (LightDetectorPermissions)Enum.Parse(                      typeof(LightDetectorPermissions), element, false);    }    else       throw new ArgumentException("XML element does not correctly " +                                   "parse to a LightDetectorPermission"); } public override SecurityElement ToXml() {    SecurityElement element = new SecurityElement("IPermission");    Type type = typeof(LightDetectorPermission);    StringBuilder assemblyName = new StringBuilder(type.Assembly.ToString());    assemblyName.Replace('\"', ' \'');    element.AddAttribute("class", type.FullName + ", " + assemblyName);    element.AddAttribute("description", "Wrox Press Light detector");    element.AddAttribute("version", "1.*");    element.AddAttribute("Unrestricted", state.ToString());    return element; } 

Microsoft has provided a helper class, SecurityElement, to assist in writing an XML element that represents a permission object and that conforms to the XML schema that the CLR's security subsystem can understand. Note that in generating this element we need to remove any double quotes from the assembly name, and replace with single quotes. The SecurityElement class automatically handles the <IPermission start of the XML element, which means that the XML emitted by our ToXml() method looks like this:

 <IPermission background-color:d9d9d9">LightDetectorPermission, Version=1.0.1.0, Culture=neutral,                     PublicKeyToken=22a8cada780967db"              description="Wrox Press Light detector" version="1.*"              Unrestricted="Read" /> 

Apart from having well-formed XML here, which should be in a format that the SecurityElement class will accept, the main requirement here is that the object should be able to reconstruct itself in the same state. In other words, the result of calling ToXml() and then calling FromXml() should always be an object that has the same state as the original object.

As well as defining the permission class, we also need to define an associated attribute in order to support declarative security. We won't actually be using the following attribute in our sample, but I've included it in the sample in order to demonstrate how you would define a permission attribute - because in general you really should always supply one in case it is needed by any client code:

 [AttributeUsageAttribute(AttributeTargets.All, AllowMultiple = true)] public class LightDetectorPermissionAttribute : CodeAccessPermissionAttribute {    LightDetectorPermissions state;    public LightDetectorPermissions Access    {       get       {           if (this.Unrestricted)              return LightDetectorPermissions.All;           return state;       }       set       {           this.state = value;           if (value == LightDetectorPermissions.All)              this.Unrestricted = true;       }    }    public LightDetectorPermissionAttribute(SecurityAction action) : base(action)    {    }    public override IPermission CreatePermission()    {       return new LightDetectorPermission(state);    } } 

Finally, here's the using statements needed for the above code to work, as well as the AssemblyKeyFile attribute that ensures the assembly will have a strong name:

 using System; using System.Security; using System.Security.Permissions; using System.Text; using System.Reflection; [assembly:AssemblyKeyFile("WroxLightDetectors.snk")] [assembly:AssemblyVersion("1.0.1.0")] 

WroxLightDetectors.snk is a key file that I generated using sn.exe, and it forms part of the downloadable code for the sample.

GeneratorXmlFiles.exe

Now we will examine the code that generates the XML file that describes the light detector security permission. Strictly speaking, this part of the sample isn't absolutely necessary: we could after all, decide what the XML representation of the permission is going to look like, make sure that LightDetectorPermission.ToXml() and LightDetectorPermission.FromXml() are written so that they emit or read this format correctly, and then independently use Notepad or some similar utility to create a text file with the correct XML in it, which can be shipped with the application. However, doing that carries the obvious risk of typos - and since we have already written a ToXml() method to generate the XML element, we may as well use this method to programmatically create the XML file to be shipped - then we can be sure our XML is correct. The code we present here will actually generate two files - a file called ReadLightDetector.xml, which represents a ReadLightPermission initialized to allow read access to the light detector, and a separate file, LightDetectorPermissions.xml, which contains the XML representation of a permission set containing this one permission. First, as usual here's the namespaces we need:

 using System; using System.IO; using System.Security; using System.Security.Permissions; 

This utility doesn't need a strong name, as it's not going to be used in any situation that requires it.

The Main() method is reasonably clear:

 [STAThread] static void Main(string[] args) {    WritePermission("LightDetectorPermission.xml");    WritePermissionSet("ReadLightDetector.xml"); } 

Now here's the method that writes out a single permission:

 static void WritePermission(string file) {    LightDetectorPermission perm = new LightDetectorPermission(                                             LightDetectorPermissions.Read);    StreamWriter sw = new StreamWriter(file);    sw.Write(perm.ToXml());    sw.Close(); } 

WritePermission() simply hooks up the output from LightDetectorPermission.ToXml() to a StreamWriter to send the XML text to a file.

WritePermisionSet() writes out a permission set:

 static void WritePermissionSet(string file) {    LightDetectorPermission perm = new LightDetectorPermission(                                            LightDetectorPermissions.Read);    NamedPermissionSet pset = new NamedPermissionSet("ReadLightDetector");    pset.Description = "Light Detector Permission Set";    pset.SetPermission(perm);    StreamWriter sw = new StreamWriter(file);    sw.Write(pset.ToXml());    sw.Close(); } 

WritePermissionSet() uses the System.Security.NamedPermissionSet class, which represents a permission set. We instantiate this class, call the NamedPermissionSet.AddPermission() method to add our custom permission, and then call the NamedPermissionSet.ToXml() method to emit the XML for the whole permission set.

The LightController Library

The LightController.dll class library is the assembly that exposes managed methods to manipulate the light detector. First, the header information for this file:

 using System; using System.Security; using System.Security.Permissions; using System.Drawing; using System.Reflection; using System.IO; [assembly: AssemblyKeyFile("WroxLightDetectors.snk")] [assembly: AssemblyVersion("1.0.1.0")] [assembly: AllowPartiallyTrustedCallers()] 

Notice the AllowPartiallyTrustedCallers attribute: this is important because the library is to be digitally signed and will be invoked by code over the network.

 public class LightController {    public Color ReadValue()    {       LightDetectorPermission perm = new LightDetectorPermission(                                          LightDetectorPermissions.Read);       perm.Demand();       return Color.FromArgb(255, 40, 30);    } } 

As you can see, I've implemented only one method in this class - that's all that's required for the sample. ReadValue() simulates the process of hooking up to the light detector and reading the detected value.

The Remote Client

The remote client is a standard Windows Forms application, with the following using directives and assembly-level attributes:

 using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.Reflection; using System.Security; [assembly: AssemblyKeyFile("WroxLightDetectors.snk")] [assembly: AssemblyVersion("1.0.1.0")] 

The fact that it has been signed with the WroxLightDetectors.snk file is important, since otherwise the program will not have sufficient permissions to call the LightController.ReadValue() method. The key code is in the event handler for clicking the button:

 private void btnRead_Click(object sender, System.EventArgs e) {    try    {       LightController controller = new LightController();       this.tbColor.Text = controller.ReadValue().ToString;    }    catch (SecurityException.ex)    {       this.tbColor.Text = "SecurityException: " + ex.Message;    }    catch (Exception ex)    {       this.tbColor.Text = "Exception: " + ex.Message;    } } 

Because this sample is aimed at demonstrating security, I've provided separate exception handlers for security exceptions and other types of exception - just to make sure that if there are any problems with the security permissions, this is picked out and a fairly clear message displayed about the problem.

Compiling the Code

Now we've seen all the source code, we need to compile it and register the new security permissions. Compilation can be done by running the following batch file, which compiles the source code, runs the XML generator, and places the two libraries in the Global Assembly Cache:

 @rem Compile assemblies and add libraries to assembly cache csc /target:library LightDetectorPermission.cs gacutil /i LightDetectorPermission.dll csc /r:LightDetectorPermission.dll /r:System.Drawing.dll /target:library LightController.cs gacutil /i LightController.dll csc /r:LightDetectorPermission.dll GenerateXmlFiles.cs csc /r:LightController.dll /r:System.Windows.Forms.dll /target:winexe LightDetector.cs @rem Run app to generate the XML files describing permission and permission set GenerateXmlFiles @rem Add permission DLL to full trust list caspol -polchgprompt off caspol -addfulltrust LightDetectorPermission.dll caspol -polchgprompt on 

The final statements in the batch file register the LightDetectorPermissions.dll as an assembly that is allowed to affect security policy. Note that the requirement to register an assembly in this way is additional to the requirement for the assembly to run under the full trust permission set, if the assembly is to define additional permissions.

In principle it would be possible to use caspol to perform the registration of the new permission set and code group from the batch file. However, I've not done that because I felt it would give a clearer picture of what's going on if we use the mscorcfg UI instead.

To start off, we need to add a new permission set. That's easily achieved by right-clicking on the PermissionSets node in mscorcfg and selecting New Permission from the context menu. We are then presented with a dialog box asking us to select the permissions to be added to this set. Because we are using a custom permission rather than one of the built-in ones, we need to click on the Import button, which brings up a file dialog allowing us to browse for the appropriate XML file. The screenshot shows the situation after we've done that. Note that at the time of writing there seems to be a problem in mscorcfg that means the description of the newly added permission doesn't appear in a very friendly format, but we don't need to worry about that:

click to expand

Now we need to add a code group. We can do this by right-clicking on the Code Groups node in the mscorcfg treeview, and selecting New Code Group from the context menu. This takes us into a couple of dialog boxes that allow us to specify the new code group - its name, its membership condition, and its permission set. The membership condition dialog looks like this:

click to expand

The precise controls that appear in the lower half of the dialog depend on the condition type for the group. If we select a Strong Name condition, we can use the Import button to browse and select an assembly that is signed with the required strong name; the public key will then be read from this assembly. The screenshot shows the situation after I have opted to read the key from LightDetectorPermission.dll.

After adding the new code group and permission set, mscorcfg looks like this:

click to expand

With security set up in this way, we can then upload the LightDetector.exe assembly to a remote location (although I used a trusted internet site when I tested the sample, a file share on the local network will do just as well). Taking a trusted zone internet site as an example, if we try to execute the file by typing in its URL in Internet Explorer, the CLR will identify the assembly as matching two zones - Trusted_Zone, which gives it permission to do things like execute and display a form, and Wrox_Light_Detectors, which gives it LightDetector read permission - so you'll find it runs successfully.

It's also an interesting exercise to remove the AssemblyKeyFile attribute from the LightDetector.cs source file, recompile and upload, so the file is not signed by a strong name. This will mean that the only code group the assembly satisfies is the Trusted_Zone one, which means it can execute and display a dialog box, but it won't be allowed the LightDetector permission. Running the application in this situation gives this result:

click to expand

Bear in mind that if you rebuild the sample, you may need to register the permission again as well, to make sure that the security policy refers to the correct, up-to-date, versions of the files.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net