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):
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):
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 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.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.
{
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.
{
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.
{
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:
{
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:
{
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:
gacutil /i timeperm.dll
caspol -af timeperm.dll
Now go back to the ProtectedData class and make these
changes:
{
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 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.
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:
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:
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:
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 ( |
Finally, clean up this example:
- remove full trust from
timeperm(select the assembly in the Policy Assemblies node, right click and select Delete, or usecaspol -rf timeperm.dll) - remove the
evidenceassembly 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.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.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):
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 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.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:
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:
{
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:
{
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:
{
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:
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:
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:
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 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):
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:
<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:
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 DiskPermSizeandcaspol -rp DiskSizePermission) - remove full trust from
diskperm(select the assembly in the Policy Assemblies node, right click and select Delete, or usecaspol -rf diskperm.dll) - remove the
diskpermassembly 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.