.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

7.3 Custom Non CAS Permissions

The permission classes provided by the framework should be adequate for most of the things you do. However, if you feel that you have a resource that must be protected and you think that there isn't a suitable class in the framework, then you can create your own permission class and extend CAS policy to use it.

There are two types of permissions: Code Access Security (CAS) permissions and non-CAS permissions. Both type of permission classes must implement IPermission so that permissions can be combined in permission sets and demands can be made. If you create your own permission class then you should make it sealed so that there is no possibility of a derived class overriding your security checks. Furthermore, permission classes should be serializable. If you want to provide declarative security then you must provide a security attribute based on your permission class. Note that if you want to have assembly security requests, link demands or inheritance demands then you must have an attribute class.

Non-CAS permissions are granted based on some general information not associated with a specific assembly and so when a demand is made the check is made on this general information and a stack walk is irrelevant. In general, you will implement a permission class and an attribute class so that you can perform declarative and imperative demands. The attribute class will create an instance of the permission class on which the runtime will invoke Demand.

The non-CAS permission object is created in an assembly through its code, this is done in two ways: by creating an instance of the permission class or by using the associated attribute. Your code calls Demand (or the runtime will call it for you on the permission object created for the attribute) and this method uses whatever information it has to determine if the demand succeeds. A non-CAS permission class must not be derived from CodeAccessPermission, because this class has the infrastructure for performing stack walks. IPermission is derived from ISecurityEncodable and so the permission class should implement the methods from this interface: ToXml and FromXml. These methods are used to write the permission to, and initialize a permission object from XML when you use declarative security.

Finally, a non-CAS permission should implement IUnrestrictedPermission and provide a constructor that takes a PermissionState parameter. This interface is used in the situation when there are unrestricted permissions, and contains a method called IsUnrestricted. The constructor with a PermissionState parameter naturally goes with IUnrestrictedPermission because IsUnrestricted indicates whether the permission is unrestricted.

Implementing an attribute permission class is similar for CAS and non-CAS permissions. In both cases you should derive from CodeAccessPermissionAttribute, the most important method of this is CreatePermission which will create an instance of a corresponding permission class. In addition, your class must have a single constructor that takes a SecurityAction parameter which should be passed to the base class constructor.

Start by creating a new library assembly (lib.cs):

using System;

public class ProtectedData
{
   const string defaultData = "privileged access data";
   string data;
   public string Data
   {
      get { return data; }
      set { data = value; }
   }
   public ProtectedData()
   {
      data = defaultData;
   }
   public override string ToString()
   {
      return "ProtectedData " + Data;
   }
}

This is a resource that will be protected by the custom permission. The code that uses it is simple (app.cs):

using System;

class App
{
   static void Main(string[] args)
   {
      ProtectedData data = new ProtectedData();
      Console.WriteLine("data is: {0}", data.Data);
      Console.WriteLine("writing data");
      data.Data = "new data";
      Console.WriteLine("written data");
   }
}

This creates a new ProtectedData object and accesses the Data object through the getter and the setter. You can compile this code and run it to convince yourself that it works.

csc /t:library lib.cs
csc app.cs /r:lib.dll

In this example the permission class will restrict access to the object to certain times of the day. If the time is within the restricted time then the demand will succeed, otherwise a security exception will be thrown. Since all assemblies in the stack will measure the same time it makes no sense to perform a stack walk: if one assembly has this permission then all the other assemblies will have this permission too.

The permission object must hold the acceptable time span and it will also have a flag to indicate if the permission is unrestricted. These values will be set up by the constructors:

using System;
using System.Security;
using System.Reflection;
using System.Security.Permissions;

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AllowPartiallyTrustedCallers]

[Serializable]
public sealed class ProtectedDataPermission
   : IPermission, IUnrestrictedPermission
{
   int startTime;
   int endTime;

   bool unrestricted = false;

   public ProtectedDataPermission(PermissionState state)
   {
      if (state == PermissionState.Unrestricted)
         unrestricted = true;
      startTime = endTime = -1;
   }
   public ProtectedDataPermission(int start, int end)
   {
      if (end < start) throw new ArgumentException("time period cannot cross midnight");
      startTime = start;
      endTime = end;
   }
   public bool IsUnrestricted()
   {
      return unrestricted;
  }
  // other members
}

The permission assembly must be put into the GAC. The reason is that in v1.1 of the framework the attribute object is created at compile time to obtain information about the permission object, and so the attribute assembly and the permission assembly, must be accessible to the compiler. The only place that you can guarantee this is if the permission assembly is in the GAC. This means that the assembly must be strong named and since the permission assembly could be accessed by partially trusted assemblies the assembly must have [AllowPartiallyTrustedCallers].

.NET Version 3.0
In Version 3.0/2.0 of the framework declarative security is not accessed at compile time, and so the permission assemblies do not have to be in the GAC.

The first constructor takes a PermissionState and if this indicates that the permission is unrestricted then anyone can have the permission at any time; if this is not unrestricted then no access is allowed. The second constructor allows you to set the times that the permission demand will succeed. To make the calculations simple, the times are expressed in 24-hour notation and we do not allow time periods that traverse midnight.

Your permission class needs to implement Union, Intersect and IsSubsetOf to support combining permissions. For this permission it gets a bit complicated because the intersection has to be the time common to both permissions, the union has to be the time covered by both, and the subset test must make sure that the time of the current permission is entirely within the time of the provided permission.

public IPermission Intersect(IPermission target)
{
   if (target == null) return null;
   ProtectedDataPermission targetItem = target as ProtectedDataPermission;
   if (targetItem == null)
      throw new ArgumentException(
         "Argument must be of type ProtectedDataPermission.");
   // If one is unrestricted then the intersection is the other
   if (this.unrestricted) return targetItem.Copy();
   if (targetItem.unrestricted) return this.Copy();
   // Return an object which is the common time of the two
   int start, end;
   if (this.startTime > targetItem.startTime)
      start = this.startTime;
   else
   start = targetItem.startTime;
   if (this.endTime < targetItem.endTime)
      end = this.endTime;
   else
      end = targetItem.endTime;
   if (start < end) // The two periods do not overlap
      return new ProtectedDataPermission(start, end);
   return new ProtectedDataPermission(PermissionState.None);
}

This must return a permission object that represents the intersection of both the current object and the one passed. The intersection is the data common to both, and in this case, it is the time where the two objects overlap. If the passed object is null then we cannot return a permission object, and if the wrong type of permission object is passed then there has been some mistake so an error is thrown. If one of the permission objects is unrestricted then the intersection of the two is the time covered by the other object. If they are both restricted then the time that is common to both is calculated by determining latest of the start times of the two and the earliest of end times of the two. If the two time spans do not intersect then a permission object with no access is returned.

public IPermission Union(IPermission other)
{
   if (other == null) return this.Copy();
   ProtectedDataPermission targetItem = other as ProtectedDataPermission;
   if (targetItem == null)
      throw new ArgumentException(
         "Argument must be of type ProtectedDataPermission.");
   // If either unrestricted then return unrestricted
   if (this.unrestricted || targetItem.unrestricted)
      return new ProtectedDataPermission(PermissionState.Unrestricted);
   // Return the time covered by both
   int start, end;
   if (this.startTime < targetItem.startTime)
      start = this.startTime;
   else
      start = targetItem.startTime;
   if (this.endTime > targetItem.endTime)
      end = this.endTime;
   else
      end = targetItem.endTime;
   if ((end - start)
      > ((this.endTime - this.startTime) + (targetItem.endTime - targetItem.startTime)))
      return new ProtectedDataPermission(PermissionState.None);
   return new ProtectedDataPermission(start, end);
}

Here the returned permission object has the contiguous time represented by both, if the two do not overlap then no access is given.

public bool IsSubsetOf(IPermission target)
{
   if (target == null) return false;
   ProtectedDataPermission targetItem = target as ProtectedDataPermission;
   if (targetItem == null)
      throw new ArgumentException(
         "Argument must be of type ProtectedDataPermission.");
   // If both are unrestricted, then they are a subset of each other
   if (this.unrestricted && targetItem.unrestricted) return true;
    // Test to see if they overlap
   if (this.endTime < targetItem.startTime) return false;
   // Now see if our times are within the times of the target
   return (this.startTime >= targetItem.startTime
      && this.endTime <= targetItem.endTime);
}

This object is a subset of the passed object only if the times overlap. If a null object is passed then you cannot create a subset and hence false is returned.

Permissions must be serializable because permission sets will be stored in the security configuration files. Thus the system will need to serialize an object to XML or initialise one from XML using these two methods:

public void FromXml(SecurityElement elem)
{
   string unrestricted = elem.Attribute("Unrestricted");
   if (unrestricted != null)
      this.unrestricted = (unrestricted == "true");
   string strStart = elem.Attribute("Start");
   if (strStart != null)
      startTime = Int32.Parse(strStart);
   string strEnd = elem.Attribute("End");
   if (strEnd != null)
      endTime = Int32.Parse(strEnd);
}
public SecurityElement ToXml()
{
   SecurityElement e = new SecurityElement("IPermission");
   e.AddAttribute("class", this.GetType().AssemblyQualifiedName);
   e.AddAttribute("version", "1");
   if (this.unrestricted)
   {
      e.AddAttribute("Unrestricted", "true");
   }
   else
   {
      e.AddAttribute("Start", startTime.ToString());
      e.AddAttribute("End", endTime.ToString());
   }
   return e;
}

The final task is to implement Demand to check to see if the current time is within the allowable time:

public void Demand()
{
   if (this.unrestricted) return;
   if (startTime == -1 && endTime == -1)
      throw new SecurityException("No access is allowed at any time");
   int hours = DateTime.Now.Hour;
   if (hours < startTime || hours >= endTime)
      throw new SecurityException("Access not allowed at this time");
   // Otherwise allow access
}

If the permission object is unrestricted then it is immaterial what time it is because access is always allowed; similarly, if no access is allowed there is no need to test the time. Finally, the time is obtained and tested to see if it is within the limits specified. The final code can be found here.

Compile this code and insert it into the GAC and add full trust:

csc /t:library timeperm.cs
gacutil /i timeperm.dll
caspol -af timeperm.dll

Now go back to the ProtectedData class and make these changes:

public class ProtectedData
{
   string data;
   public string Data
   {
      get
      {
         // Read access during your office hours
         ProtectedDataPermission perm = new ProtectedDataPermission(8, 18);
         perm.Demand();
         return data;
      }
      set
      {
         // Write access during your manager's office hours
         ProtectedDataPermission perm = new ProtectedDataPermission(10, 17);
         perm.Demand();
         data = value;
      }
   }
   public ProtectedData()
   {
      data = "<empty>";
   }
   public ProtectedData(string d)
   {
      data = d;
   }
   public override string ToString()
   {
      return "ProtectedData " + data;
   }
}

The code demands an appropriate permission. Compile the code:

csc /t:library lib.cs /r:timeperm.dll
csc app.cs /r:lib.dll

Now run the code. If you run this code between 10am and 5pm you'll find that it will work with no exceptions. If you run the code between 8am and 10am or between 5pm and 6pm you'll find that you'll get an exception when trying to write to the property. If you call this code before 8am or after 6pm then the exception will be thrown when you try to read the property. Of course, I know that you will be a manager and will be reading this at work, so this code will always work <g> therefore to test this code correctly you'll have to change the times in the permission objects.

Declarative demands are simple to implement for non-CAS permission. To do this add the following class to timeperm.cs.

[Serializable,
 AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property,
    AllowMultiple = true, Inherited = false)]
public sealed class ProtectedDataPermissionAttribute : CodeAccessSecurityAttribute
{
   int startTime;
   int endTime;
   bool unrestricted = false;

   public ProtectedDataPermissionAttribute(SecurityAction action)
      : base(action)
   {
      startTime = -1;
      endTime = -1;
   }

   public new bool Unrestricted
   {
      get { return unrestricted; }
      set { unrestricted = value; }
   }

   public int StartTime
   {
      get { return startTime; }
      set { startTime = value; }
   }

   public int EndTime
   {
      get { return endTime; }
      set { endTime = value; }
   }

   public override IPermission CreatePermission()
   {
      if (Unrestricted)
      {
         return new ProtectedDataPermission(PermissionState.Unrestricted);
      }
      else
      {
         if (endTime < startTime || endTime == -1 || startTime == -1)
            return new ProtectedDataPermission(PermissionState.None);
         return new ProtectedDataPermission(startTime, endTime);
      }
   }
}

The constructor must have a SecurityAction parameter, so any other data required by the permission object must be assigned using properties. Because of this the attribute must have some failsafe mechanism to make sure that valid data is used to create the ProtectedDataPermission object. In this case, the startTime and endTime fields are initialised with -1 to indicate that they do not have valid values. CreatePermission checks these values and if they are invalid then the ProtectedDataPermission object is created to prevent any access. Now edit lib.cs to use the attribute:

[ProtectedDataPermission(SecurityAction.Demand, StartTime=8, EndTime=18)]
get
{
   // Read access during your office hours
   return data;
}
[ProtectedDataPermission(SecurityAction.Demand, StartTime = 10, EndTime = 17)]
set
{
   // Write access during your manager's office hours
   data = value;
}

Compile the permission assembly and add it to the GAC, then compile the library and the process:

csc /t:library timeperm.cs
gacutil /i timeperm.dll
csc /t:library lib.cs /r:timeperm.dll
csc app.cs /r:lib.dll

Now run the application to show that you have the same behaviour as before: the attributes specify when you can access the property.

Before doing anything else, use ILDASM to view the property get and set methods, for example here's the get method:

.method public hidebysig specialname instance string
get_Data() cil managed
{
.permissionset demand = (
3C 00 50 00 65 00 72 00 6D 00 69 00 73 00 73 00 // <.P.e.r.m.i.s.s.
69 00 6F 00 6E 00 53 00 65 00 74 00 20 00 63 00 // i.o.n.S.e.t. .c.
6C 00 61 00 73 00 73 00 3D 00 22 00 53 00 79 00 // l.a.s.s.=.".S.y.
73 00 74 00 65 00 6D 00 2E 00 53 00 65 00 63 00 // s.t.e.m...S.e.c.
75 00 72 00 69 00 74 00 79 00 2E 00 50 00 65 00 // u.r.i.t.y...P.e.
72 00 6D 00 69 00 73 00 73 00 69 00 6F 00 6E 00 // r.m.i.s.s.i.o.n.
53 00 65 00 74 00 22 00 0D 00 0A 00 20 00 20 00 // S.e.t."..... . .
20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // . . . . . . . .
20 00 20 00 20 00 20 00 20 00 76 00 65 00 72 00 // . . . . .v.e.r.
73 00 69 00 6F 00 6E 00 3D 00 22 00 31 00 22 00 // s.i.o.n.=.".1.".
2F 00 3E 00 0D 00 0A 00 )                       // /.>.....
.permissionset noncasdemand = (
3C 00 50 00 65 00 72 00 6D 00 69 00 73 00 73 00 // <.P.e.r.m.i.s.s.
69 00 6F 00 6E 00 53 00 65 00 74 00 20 00 63 00 // i.o.n.S.e.t. .c.
6C 00 61 00 73 00 73 00 3D 00 22 00 53 00 79 00 // l.a.s.s.=.".S.y.
73 00 74 00 65 00 6D 00 2E 00 53 00 65 00 63 00 // s.t.e.m...S.e.c.
75 00 72 00 69 00 74 00 79 00 2E 00 50 00 65 00 // u.r.i.t.y...P.e.
72 00 6D 00 69 00 73 00 73 00 69 00 6F 00 6E 00 // r.m.i.s.s.i.o.n.
53 00 65 00 74 00 22 00 0D 00 0A 00 20 00 20 00 // S.e.t."..... . .
20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // . . . . . . . .
20 00 20 00 20 00 20 00 20 00 76 00 65 00 72 00 // . . . . .v.e.r.
73 00 69 00 6F 00 6E 00 3D 00 22 00 31 00 22 00 // s.i.o.n.=.".1.".
3E 00 0D 00 0A 00 20 00 20 00 20 00 3C 00 49 00 // >..... . . .<.I.
50 00 65 00 72 00 6D 00 69 00 73 00 73 00 69 00 // P.e.r.m.i.s.s.i.
6F 00 6E 00 20 00 63 00 6C 00 61 00 73 00 73 00 // o.n. .c.l.a.s.s.
3D 00 22 00 50 00 72 00 6F 00 74 00 65 00 63 00 // =.".P.r.o.t.e.c.
74 00 65 00 64 00 44 00 61 00 74 00 61 00 50 00 // t.e.d.D.a.t.a.P.
65 00 72 00 6D 00 69 00 73 00 73 00 69 00 6F 00 // e.r.m.i.s.s.i.o.
6E 00 2C 00 20 00 74 00 69 00 6D 00 65 00 70 00 // n.,. .t.i.m.e.p.
65 00 72 00 6D 00 2C 00 20 00 56 00 65 00 72 00 // e.r.m.,. .V.e.r.
73 00 69 00 6F 00 6E 00 3D 00 31 00 2E 00 30 00 // s.i.o.n.=.1...0.
2E 00 30 00 2E 00 30 00 2C 00 20 00 43 00 75 00 // ..0...0.,. .C.u.
6C 00 74 00 75 00 72 00 65 00 3D 00 6E 00 65 00 // l.t.u.r.e.=.n.e.
75 00 74 00 72 00 61 00 6C 00 2C 00 20 00 50 00 // u.t.r.a.l.,. .P.
75 00 62 00 6C 00 69 00 63 00 4B 00 65 00 79 00 // u.b.l.i.c.K.e.y.
54 00 6F 00 6B 00 65 00 6E 00 3D 00 63 00 37 00 // T.o.k.e.n.=.c.7.
36 00 31 00 31 00 66 00 38 00 36 00 31 00 34 00 // 6.1.1.f.8.6.1.4.
33 00 38 00 30 00 65 00 64 00 33 00 22 00 0D 00 // 3.8.0.e.d.3."...
0A 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // .. . . . . . . .
20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // . . . . . . . .
20 00 76 00 65 00 72 00 73 00 69 00 6F 00 6E 00 // .v.e.r.s.i.o.n.
3D 00 22 00 31 00 22 00 0D 00 0A 00 20 00 20 00 // =.".1."..... . .
20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // . . . . . . . .
20 00 20 00 20 00 20 00 20 00 20 00 53 00 74 00 // . . . . . .S.t.
61 00 72 00 74 00 3D 00 22 00 38 00 22 00 0D 00 // a.r.t.=.".8."...
0A 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // .. . . . . . . .
20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 // . . . . . . . .
20 00 45 00 6E 00 64 00 3D 00 22 00 31 00 38 00 // .E.n.d.=.".1.8.
22 00 2F 00 3E 00 0D 00 0A 00 3C 00 2F 00 50 00 // "./.>.....<./.P.
65 00 72 00 6D 00 69 00 73 00 73 00 69 00 6F 00 // e.r.m.i.s.s.i.o.
6E 00 53 00 65 00 74 00 3E 00 0D 00 0A 00 )     // n.S.e.t.>.....

As you can see there is a metadata CAS demand for an empty permission set and a non-CAS demand for ProtectedDataPermission. This information is obtained at compile time by the compiler by creating an instance of ProtectedDataPermission, initializing it with the information in the attribute and then calling ToXml. At runtime this XML is extracted from metadata and an instance of the permission type (ProtectedDataPermission) is created and then initialized with the XML by passing it to FromXml.

.NET Version 3.0
After you have compiled the library look at it with ILDASM, the results for the get method are as follows (whitespace added by me):

.method public hidebysig specialname instance string
get_Data() cil managed
{
.permissionset demand =
   {
      [timeperm]ProtectedDataPermissionAttribute =
      {
         property int32 'StartTime' = int32(8)
         property int32 'EndTime' = int32(18)
      }
    }

This takes a similar format to custom attributes (.custom) in that there is the fully qualified name of a class and then the values of properties that should be used to initialize an instance of that class. This information, the class name and the name of the properties and their values, can be obtained by the compiler at compile time from metadata, that is, without having to create an instance of the permission class. During runtime the .NET runtime can create an instance of the permission object, initialize it and then call Demand.

Finally, clean up this example:

  • remove full trust from timeperm (select the assembly in the Policy Assemblies node, right click and select Delete, or use caspol -rf timeperm.dll)
  • remove the evidence assembly from the GAC (gacutil -u timeperm).

7.4 Custom CAS Permissions

A CAS permission is granted based on the identity of the assembly, so a separate check must be made on each assembly by policy to determine if it can have the permission. There are two places where the permission object is created, the most obvious place is within your code: you create an instance to perform a demand (or one of the stack walk modifiers) or the runtime creates an instance when it sees the associated attribute. The other place where the CAS permission object is created is when the runtime creates a permission set for an assembly via policy based on the evidence of the assembly. When a demand is made and a stack walk is performed the granted permissions will be compared with the permission object created in (or for) your method. If the method's permission object is a subset of the equivalent permission object in the granted permission set then the permission demand succeeds. Clearly IPermission.IsSubsetOf is vital to the way that stack walks work.

The framework provides the CodeAccessPermission class and CodeAccessSecurityAttribute class with common code that you can use as base classes for your CAS permission. Extending CAS permissions requires deriving from these classes and overriding their members to handle your resource. Do not be tempted to implement Demand yourself because a demand for a CAS permission should initiate a stack walk. The framework classes do this by calling an internal class called CodeAccessSecurityEngine and since it is internal to mscorlib you will not have access to this class.

This example will develop a CAS permission to control access to a class that gives you information about the disks on your machine. Before showing the code I want to have a short rant about the support in v1.1 framework for disk information. The System.IO namespace should have a class to give information about the disks on your machine, but it doesn't (this is the case for 1.1, for 3.0/2.0, see the box below). The only information you can get about the drives on your machine is through Directory.GetLogicalDrives and Environment.GetLogicalDrives. These have the same code (a wrapper around the Win32 GetLogicalDrives function in kernel32.dll) with one important difference: the former demands a SecurityPermission for UnmanagedCode whereas the latter demands unrestricted EnvironmentPermission.

To get more information about a disk MSDN library recommends that you use the WMI classes in System.Management. This is typical of the woolly thinking from Microsoft about .NET. There are many unmanaged functions in kernel32.dll that you can use through platform invoke (for example, GetDiskFreeSpaceEx) and since kernel32.dll will be automatically loaded in your process as part of the .NET runtime, calling these methods comes with little performance cost. However, the WMI classes are a different case altogether. Firstly, WMI is a COM technology so that the System.Management classes will have to perform COM interop. COM interop is equivalent in performance terms to platform invoke, so this behaviour of the WMI classes is no worse than calling the Win32 disk functions yourself. The big problem with WMI is that the COM objects are implemented by a COM local server which means that they are implemented in another process and so to access them requires interprocess communication and COM marshalling between apartments. All this adds up to far worse performance than calling a DLL that is already in your process. Whoever decides to recommend using WMI when a Win32 function exists clearly isn't thinking straight.

.NET Version 3.0
In fact, version 3.0/2.0 of the framework library has a new class called DriveInfo which addresses this issue, but since I have already developed the class, I will keep it as the example in this section.

Here's a class to get the amount of free space on a disk given its name (lib.cs):

using System;
using System.Security;
using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AllowPartiallyTrustedCallers]

public class DiskInfo
{
   [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
   static extern bool GetDiskFreeSpaceEx(
      string drive, out long freeBytesForUser,
      out long totalBytes, out long freeBytes);
   [DllImport("kernel32")]
   static extern int SetErrorMode(int newMode);

   public static long AvailableFreeSpace(string disk)
   {
      int errorMode = SetErrorMode(1);
      long freeBytes, totalBytes, totalFreeBytes;
      try
      {
         if (!GetDiskFreeSpaceEx(disk, out freeBytes, out totalBytes, out totalFreeBytes))
         {
            throw new ArgumentException(
               String.Format("cannot get free space for {0} 0x{1:x8}",
                  disk, Marshal.GetLastWin32Error()));
         }
      }
      finally
      {
         SetErrorMode(errorMode);
      }
   return freeBytes;
   }
}

GetDiskFreeSpaceEx will access the disk to determine how large it is and how much is free. The problem is that removable drives, like floppy drives or CD drives, may not have disks in them. If this occurs for then a dialog will be displayed requesting that you put a disk in the drive. The call to SetErrorMode with a value of 1 will prevent this dialog being shown and will set the last error value. Setting SetLastError to true in the [DllImport] attribute means that your code can call GetLastWin32Error to get this error value.

The library has been signed and has [AllowPartiallyTrustedCallers] so that a strong named, partially trusted assembly can call it.

So that you can test this later you'll need to call it from a partially trusted assembly, caller.cs, that just acts as an intermediary:

using System;
using System.Reflection;

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

public class Caller
{
   public static long GetFreeSpace(string drive)
   {
      return DiskInfo.AvailableFreeSpace(drive);
   }
}

This has a strong name so that later on in this section you can download this assembly so that it gets partial trust. The code that uses this library is simple (app.cs):

using System;

class App
{
   static void Main()
   {
      string[] drives = Environment.GetLogicalDrives();
      foreach(string drive in drives)
      {
         try
         {
            long freeSpace = Caller.GetFreeSpace(drive);
            Console.WriteLine("{0} has {1} bytes free", drive, freeSpace);
         }
         catch(ArgumentException e)
         {
            Console.WriteLine("error: {0}", e.Message);
         }
      }
   }
}

Compile this code and confirm that it will return the sizes of the disks on your machine:

csc /t:library lib.cs
csc /t:library caller.cs /r:lib.dll
csc app.cs /r:caller.dll

Clearly to call GetDiskFreeSpaceEx your code must have UnmanagedCode permission. However, this permission means that you will have access to every Win32 method. Just because your code has access to (for example) the method to access environment variables should your code also have access to information about your disks? Therefore, it makes sense to provide a separate permission for accessing information about disk information. Furthermore, if your assembly has this permission it will give access to information that you may not want other assemblies to have, so this permission should initialise a stack walk to make sure that all assemblies have this permission. Thus, this is an ideal candidate for a CAS permission.

Start by creating a library called diskperm.cs, as explained earlier, for v1.1 of the framework the permission assembly must be in the GAC, and it must allow access to partial trust callers.

using System;
using System.Reflection;
using System.Security;
using System.Security.Permissions;

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

[assembly: AllowPartiallyTrustedCallers]

[Flags, Serializable]
public enum DiskAccess{ None = 0, Size = 1, Label = 2, Type = 4, All = 7 }

[Serializable]
public sealed class DiskPermission : CodeAccessPermission, IUnrestrictedPermission
{
}

The enumeration defines what the permission object allows: to get the size of the disk, get its name, or get the type of the disk. The DiskInfo object only gives information about the size of the disk, so the other values can be used for future expansion. The permission object must implement a constructor with PermissionState and it makes sense to have a constructor to initialise the object according to the access required:

[Serializable]
public sealed class DiskPermission : CodeAccessPermission, IUnrestrictedPermission
{
   DiskAccess diskAccess;

   public DiskPermission(PermissionState state)
   {
      if (state == PermissionState.Unrestricted) diskAccess = DiskAccess.All;
         else diskAccess = DiskAccess.None;
   }
   public DiskPermission(DiskAccess access)
   {
      diskAccess = access;
   }
   public bool IsUnrestricted()
   {
      return (diskAccess == DiskAccess.All);
   }
   public DiskAccess Access
   {
      get { return diskAccess; }
      set { diskAccess = value; }
   }

Since DiskAccess is a bitmap (hence [Flags]) calculating the intersection and union are straightforward:

public override IPermission Copy()
{
   return new DiskPermission(diskAccess);
}
public override IPermission Intersect(IPermission target)
{
   if (target == null) return null;
   DiskPermission targetItem = target as DiskPermission;
   if (targetItem == null)
      throw new ArgumentException("Argument must be of type DiskPermission");
   // If either is DiskAccess.None then the intersection is None
   if (this.diskAccess == DiskAccess.None
      || targetItem.diskAccess == DiskAccess.None)
   {
      return new DiskPermission(DiskAccess.None);
   }
   // DiskAccess is a bitmap so the intersection is simply a logical AND
   return new DiskPermission(this.diskAccess & targetItem.diskAccess);
}
public override IPermission Union(IPermission target)
{
   if (target == null) return null;
   DiskPermission targetItem = target as DiskPermission;
   if (targetItem == null)
      throw new ArgumentException("Argument must be of type DiskPerm");
   // DiskAccess is a bitmap so the union is simply a logical OR
   return new DiskPermission(this.diskAccess | targetItem.diskAccess);
}

The methods to serialize and deserialize to XML are also straightforward:

public override SecurityElement ToXml()
{
   SecurityElement e = new SecurityElement("IPermission");
   e.AddAttribute("class", this.GetType().AssemblyQualifiedName);
   e.AddAttribute("version", "1");
   if (this.diskAccess == DiskAccess.All)
      e.AddAttribute("Unrestricted", "true");
   else
      e.AddAttribute("Access", diskAccess.ToString());
   return e;
}
public override void FromXml(SecurityElement elem)
{
   string unrestricted = elem.Attribute("Unrestricted");
   if (unrestricted != null)
   {
      if (unrestricted == "true")
      {
         this.diskAccess = DiskAccess.All;
         return;
      }
   }
   string strAccess = elem.Attribute("Access");
   if (strAccess != null)
      diskAccess = (DiskAccess)Enum.Parse(typeof(DiskAccess), strAccess);
}

The final method is to determine if the current object is a subset of the object passed as a parameter. Clearly, if the passed object has no access (None) then the test should fail. Conversely, if the passed object has All access then the test should succeed (again, except if the current object has None). If the two have the same value (except for None) then the test should succeed. Here's the code:

public override bool IsSubsetOf(IPermission target)
{
   if (target == null) return false;
   DiskPermission targetItem = target as DiskPermission;
   if (targetItem == null)
      throw new ArgumentException("Argument must be of type DiskPerm");
   // If either is DiskAccess.None then the result is false
   if (this.diskAccess == DiskAccess.None
      || targetItem.diskAccess == DiskAccess.None)
   {
      return false;
   }
   // If there is more access in this than the target, then the check should fail
   if (this.diskAccess > targetItem.diskAccess)
   {
      return false;
   }
   // If both are the same then result is true
   if (this.diskAccess == targetItem.diskAccess)
   {
      return true;
   }
   // If the target is All then the result is true
   if (targetItem.diskAccess == DiskAccess.All)
   {
      return true;
   }
   // All other cases are false
   return false;
}

The next task is to add an attribute class, this is similar to the case with non-CAS permissions:

[Serializable,
 AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property,
    AllowMultiple = true, Inherited = false)]
public sealed class DiskPermissionAttribute : CodeAccessSecurityAttribute
{
   DiskAccess diskAccess = DiskAccess.None;

   public DiskPermissionAttribute(SecurityAction action)
   : base(action)
   {
   }
   public new bool Unrestricted
   {
      get { return diskAccess == DiskAccess.All; }
      set
      {
         if (value) diskAccess = DiskAccess.All;
         else diskAccess = DiskAccess.None;
      }
   }

   public DiskAccess Access
   {
      get { return diskAccess; }
      set { diskAccess = value; }
   }

   public override IPermission CreatePermission()
   {
      return new DiskPermission(diskAccess);
   }
}

The final code can be found here. Compile the assembly and put it in the GAC:

csc /t:library diskperm.cs
gacutil /i diskperm.dll

Finally, you can use the attribute in the library to start a demand. Add a using statement for System.Security.Permissions and then add the following:

[DiskPermission(SecurityAction.Demand, Access=DiskAccess.Size)]
public static long AvailableFreeSpace(string disk)
{
   SecurityPermission sp = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode);
   sp.Assert();

The attribute demands our custom permission, but notice the Assert. The Assert is there because the call through platform invoke will require the UnmanagedCode permission and normally this would start a stack walk. If this assembly is called by a partially trusted assembly (as we expect it to be) the demand for this permission will fail. The Assert will stop this stack walk and allow the call to be made to GetDiskFreeSpaceEx. Note that this is not opening a security hole because the demand for UnmanagedCode will be replaced with a demand for DiskPermission, and the Assert will only be applied for the scope of the method.

You can now compile this code to create the final application.

csc /t:library lib.cs /r:diskperm.dll
csc /t:library caller.cs /r:lib.dll
csc app.cs /r:caller.dll

An assembly gets a CAS permission through policy and so you must add this permission to an appropriate policy. You'll want to add a permission based on the new permission, however, the configuration tool does not know about the permission class and so you cannot use the usual wizard interface. Instead, you have to do it through an XML file.

Create the following file (diskPerm.xml):

<PermissionSet class="System.Security.NamedPermissionSet"
   version="1"
   Name="DiskSizePermission"
   Description="Allows you to access the size of a disk">
   <IPermission class="DiskPermission, diskperm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c7611f8614380ed3"
      version="1"
      Access="Size"/>
</PermissionSet>

This XML identifies the permission with a fully qualified name, but since it will be used as part of the security policy the assembly (diskperm) must be fully trusted. To do this open the configuration tool, select the Policy Assemblies node and through the context menu select Add.... This will list all the assemblies in the GAC and so from this select the diskperm assembly. (You can also do this on the command line with caspol -af diskperm.dll)

.NET Version 3.0
In Version 3.0/2.0 of the framework you do not have to make the assembly fully trusted like this, since it is in the GAC it will be fully trusted anyway. If you try to add the assembly to the Policy Assemblies node you'll get the odd error: Unable to add the selected assembly. The assembly must have a strong name (name, version and public key), but caspol will give you a more meaningful error: it will say that it makes no sense to give the assembly full trust.

Now you can add the new permission set. To do this select the Machine policy, then open the Permission Sets node and from the context menu select New. This will open a new dialog with two options, select the second one, Import a permission set from an XML file and then click on the Browse button. Select the XML file that you created, click on Open and then Finish. If you get a dialog saying The XML is invalid or does not contain a permission set then there is an error in the file. Here are some things to check:

  • Check that the XML is well formed, that is, each element has a closing tag.
  • Check that the assembly has a valid name, in particular, pay attention to the PublicKeyToken
  • Check that the name of the assembly is not split over two lines
  • Check that the assembly is in the GAC

You should find that a new permission set will be added to the policy:

Now you need to get this permission allocated to your assembly. Since caller has a strong name we can deploy it to the local web server and use a configuration file to download it from that server. Create a new code group for this library so that libraries with the same strong name as caller will get DiskSizePermission. To do this, open Code Groups for the policy, then select All_Code and from the context menu select New. Give this a name (DiskPermSize) then click on Next. On the next page select Strong Name for the condition, click on Import and browse for, and select caller.dll. Click on OK, then Next and from the permission set dropdown box select DiskSizePermission. Click on Next and then Finish.

.NET Version 3.0
As mentioned before, the .NET 2.0 configuration tool often creates new code groups with a name prefixed with Copy of, this is benign but you may want to rename the code group to remove this prefix.

You can now use the Evaluate Assembly (on the context menu of Runtime Security Policy) to check that the assembly will get the required permission set (at this point simply use the Browse button to locate the assembly on the hard disk). Since the DiskPermSize code group does not have the Exclusive property checked it means that assemblies that satisfy the strong name condition will get  DiskSizePermission and the other permission sets assigned by policy. This is intentional: we want to add a permission, not replace permissions.

Now we need to make the caller assembly partially trusted (the full details are outlined in the Fusion workshop). To do this move the assembly to a folder under the IIS root folder (move caller.dll \inetpub\wwwroot\bin). Now open the application under the Applications node in the configuration tool and select Configured Assemblies. From the context menu select Add, select the top option and click on Choose Assembly and select caller. Finally click on Select and then Finish and the property pages will be shown for the library. Select the Codebases tab and in the left hand column add the version 1.0.0.0 and next to that add the URI http://localhost/bin/caller.dll. Finally click on OK. This creates the following configuration file:

<?xml version="1.0"?>
<configuration>
 <runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
   <dependentAssembly>
    <assemblyIdentity name="caller" publicKeyToken="c7611f8614380ed3" />
    <publisherPolicy apply="yes" />
    <codeBase version="1.0.0.0" href="http://localhost/bin/caller.dll" />
   </dependentAssembly>
  </assemblyBinding>
 </runtime>
</configuration>

Now you have set up a code group so that the caller assembly will get the DiskSizePermission permission. You can test this out by running the application and confirming that it will work as expected. Are you convinced that the code has the permission? To show that the permission is being granted, edit the code group you just added and change the granted permission set to Nothing. To do this, select the DiskPermSize code group and from the context menu select Properties; click on the Permission Set table and from the Permission set dropdown box select Nothing. Select OK. Run the application again and this time you'll get the following:

Unhandled Exception: System.Security.SecurityException: Request for the permission of type DiskPermission, diskperm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c7611f8614380ed3 failed.
   at System.Security.CodeAccessSecurityEngine.CheckTokenBasedSetHelper(Boolean ignoreGrants, TokenBasedSet grants, TokenBasedSet denied, TokenBasedSet demands)
   at System.Security.CodeAccessSecurityEngine.CheckSetHelper(PermissionSet grants, PermissionSet denied, PermissionSet demands)
   at DiskInfo.AvailableFreeSpace(String disk)
   at Caller.GetFreeSpace(String drive)
   at App.Main()

The state of the failed permission was:
<IPermission class="DiskPermission, diskperm, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c7611f8614380ed3"
   version="1"
   Access="Size"/>

Finally, clean up this example:

  • remove the code group DiskPermSize and the permission set DiskSizePermission using the configuration tool (or on the command line: caspol -rg DiskPermSize and caspol -rp DiskSizePermission)
  • remove full trust from diskperm (select the assembly in the Policy Assemblies node, right click and select Delete, or use caspol -rf diskperm.dll)
  • remove the diskperm assembly from the GAC (gacutil -u diskperm).
I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Eight

This page is (c) 2007 Richard Grimes, all rights reserved