There are many permissions defined in the System.Security.Permissions namespace of the .NET Framework class library, although it is expected that they will not meet all the needs of future secure class libraries. Thus, the security system of the .NET Platform was designed to be extensible so that permissions could be added by .NET developers.
If a secured assembly is going to use a custom permission, it is crucial to test the permission class in addition to the secured methods and properties in the secured assembly. After the permission class is added to the security policy, it becomes a trusted piece of the system. If it is designed poorly, this permission could open serious security holes.
The following are major considerations when testing a custom permission:
Testing the key methods of a custom permission that interface with the security system
Testing imperative use of a custom permission
Testing declarative use of a custom permission
When a custom permission is written, is it generally written as a class that inherits from System.Security.CodeAccessPermission. There are a few key methods that the custom permission class overrides from the CodeAccessPermission base class. These methods are called by the security system of the CLR to enforce the permission. These methods are as follows:
Union— Creates a new permission that covers all of the two original permissions. (In Figure 26.2, the union of Permission 1 and Permission 2 is the sum of all areas A, B, C, and D.)
Intersect— Creates a new permission that covers the common subset of the two original permissions. (In Figure 26.2, the intersection of Permission 1 and Permission 2 is area C.)
IsSubsetOf— Returns true or false to show if one permission is a subset of another. (In Figure 26.2, Permission 3 is a subset of Permission 2.)
Copy— Creates a new permission that is identical to the original permission.
ToString— Prints out a textual representation of a permission.
FromXml— Changes an XML representation of a permission to a permission object itself.
When testing these methods, it is important to use a wide representation of possible permission objects. This means using null (when possible), an empty permission, an unrestricted permission, and a variety of states in between. If all states of a permission can be easily enumerated (for example, the permission state is really just represented by a few Boolean flags, such as SecurityPermission), go ahead and use all possible states. If the permission has an infinite number of possible states (for example, FileIOPermission), at least try to get a good representation of different permission states for testing.
There is one primary principle to test with Union. If C is the union of A and B, A.IsSubsetOf(C) and B.IsSubsetOf(C) should both be true. In addition, there are a few secondary principles that ensure precise behavior for boundary conditions.
The union of A and B should never return references to the A or B objects themselves. It should always return null or create a new custom permission object to return.
The union of A and null should be a copy of A.
The union of A with an empty custom permission (for example, one created with PermissionState.None) should be a copy of A.
The union of A with an unrestricted custom permission (for example, one created with PermissionState.Unrestricted) should be an unrestricted custom permission.
If A and B are different types (that is, they are instances of different classes), A.Union(B) should throw an exception unless an explicit design decision was made to allow this for the custom permission.
There is one primary principle to test with Intersect. If C is the non-null intersection of A and B, C.IsSubsetOf(A) and C.IsSubsetOf(B) should both be true. Just as with Union, there are a few secondary principles that ensure precise behavior for boundary conditions.
The intersection of A and B should never return references to the A or B objects themselves. It should always return null or create a new custom permission object to return.
The intersection of A and null should be null.
The intersection of A and an empty custom permission (for example, one created with PermissionState.None) should be an empty custom permission.
The intersection of A and an unrestricted custom permission should be a copy of A.
If A and B are different types, A.Intersect(B) should throw an exception unless an explicit design decision was made to allow this for the custom permission.
IsSubsetOf is generally tested by testing the other key methods of a custom permission. However, there are some interesting cases to make sure to test directly. The examples use some given custom permission object A and an unrestricted custom permission object B.
A.IsSubsetOf(B) should return true.
B.IsSubsetOf(A) should return false.
A.IsSubsetOf(null) should return true if A is an empty permission (for example, one created with PermissionState.None).
If A and C are different types, A.IsSubsetOf(C) should throw an exception unless an explicit design decision was made to allow this for the custom permission.
Essentially, testing Copy just requires creating a range of different kinds of permissions, making copies of them via this method, and ensuring the copy is identical to the original. Note that you cannot ensure permission objects are identical by calling the Equals method, because permission objects do not override that method. Instead, you can determine equality of two permissions A and B by calling A.IsSubsetOf(B) and B.IsSubsetOf(A). If A and B are identical permissions, both calls should return true.
CAUTION
Calling A.Equals(B) doesn't tell you if the internal permission states of A and B are identical. A.Equals(B) tells you if A and B are references to the same object in memory. To see if internal states of A and B are identical, you should call A.IsSubsetOf(B) and B.IsSubsetOf(A). If both calls are true, A and B are identical permissions.
This method is less critical than the rest of the methods listed here because no runtime decisions are based on the output. However, ToString is often used in debugging, so it is important to get it right.
The output of this method is highly dependent on the custom permission itself, so there is little guidance to give for testing. Returning anything non-null should be safe, but you should try viewing the output for several different permission objects and ensure that they make sense.
Guidance for testing ToXml and FromXml is put together because testing them is best done together. There is little need to examine the SecurityElements returned by ToXml or constructing strange SecurityElements to pass to FromXml because the custom permission should only have to consume SecurityElements in FromXml that it produced via ToXml.
Generally, the best way to test these methods is the following:
1. |
Create an instance of the custom permission. |
2. |
Call ToXml on that custom permission object. |
3. |
Call FromXml on the SecurityElement returned by ToXml. |
4. |
Compare the original custom permission object to the one resulting from the call to FromXml. |
The original custom permission object and the resulting custom permission object should be identical. As in the case of testing Copy, testing equality should be done using IsSubsetOf. So, if B is the result of calling ToXml followed by a call to FromXml, A.IsSubsetOf(B) and B.IsSubsetOf(A) should both return true. The real key to testing ToXml and FromXml is to use a wide variety of different custom permission states.
When you know the key methods of a custom permission work correctly, you have gone a long way to making sure the permission can be used properly. However, a permission used declaratively executes a somewhat different code path compared to when it is used imperatively. Thus, it is important to ensure to test the permission in both cases.
Testing imperative use of a custom permission consists of creating objects of your custom permission class and calling the following methods on those objects:
Assert
Deny
PermitOnly
Demand
You should be able to reuse the same permission states for these methods that were used for the previously tested methods. Using a wide variety of permission states will ensure robustness of the custom permission.
To test that Assert works properly for the custom permission, a good test framework to make is shown in Listing 26.1. Following this paragraph are the main scenarios to try using that framework. The individual scenarios refer to two custom permission states A and B where A.IsSubsetOf(B) is true. Also, a third custom permission, state C, is used where A.Intersect(C) returns an empty custom permission.
Assert A and Demand A should not throw a SecurityException.
Assert A and Demand B should throw a SecurityException.
Assert A and Demand C should throw a SecurityException.
CAUTION
If Assert and Demand are called in the same method, the Assert will not affect the resulting stack walk. After calling Assert, the test must call another method that calls Demand. This is because when a call is made to Demand, the stack walk begins with the frame above the frame containing the Demand. See Chapter 7, “Walking the Stack,” for more details on stack walking.
This same principle also applies for Deny and PermitOnly. Calls to Deny and PermitOnly will not affect calls to Demands in the same method.
For Deny, see Listing 26.2 for a testing framework. Following this paragraph are the main scenarios to try in that framework. The individual scenarios refer to two custom permission states A and B where A.IsSubsetOf(B) is true. Also, a third custom permission state, C, is used where A.Intersect(C) returns an empty custom permission.
Deny A and Demand A should throw a SecurityException.
Deny A and Demand B should throw a SecurityException.
Deny A and Demand C should not throw a SecurityException.
Deny B and Demand A should throw a SecurityException.
For PermitOnly, see Listing 26.3 for a testing framework. Following this paragraph are the main scenarios to try in that framework. The individual scenarios refer to two custom permission states A and B where A.IsSubsetOf(B) is true. Also, a third custom permission state, C, is used where A.Intersect(C) returns an empty custom permission.
PermitOnly A and Demand A should not throw a SecurityException.
PermitOnly A and Demand B should throw a SecurityException.
PermitOnly A and Demand C should throw a SecurityException.
PermitOnly B and Demand A should not throw a SecurityException.
Testing Demand was included while testing Assert, Deny, and PermitOnly, so no additional work should be necessary.
Declarative testing is similar to imperative testing. However, in this case, there is an expanded set of uses to try with the permission:
Assert
Deny
PermitOnly
Demand
LinkDemand
InheritanceDemand
Assert, Deny, PermitOnly, and Demand are generally the same imperatively as they are declaratively. Thus, the previous scenarios for Assert, Deny, PermitOnly, and Demand can be used for testing the custom permission declaratively. Also, the test frameworks can be easily modified to change the imperative calls to declarative calls. Listing 26.4 shows how this would be done for Assert. Note that the exact declarative representation of a permission depends on its attribute class, so this example will need to be modified to match your custom permission attribute class. Also, instead of constructing permissions in the TestDriver method and calling RunTest many times, several RunTest methods will be necessary because each one will need a different declarative security attribute.
The reason to test Assert, Deny, PermitOnly, and Demand declaratively in addition to imperatively relates to how the permissions are constructed. For imperative security, the test code creates the custom permission object itself. For declarative security, the custom permission object is created using the custom permission's attribute class. Thus, a bug in the declarative attribute class will only show up when using the custom permission declaratively.
NOTE
The two exclusive uses of permissions in declarative security, LinkDemand and InheritanceDemand, are described in more detail in Chapter 6, “Permissions: The Workhorse of Code Access Security.”
What follows are the key scenarios to check for a LinkDemand. All the scenarios will refer to method a in class A located in assembly 1, methods b1 and b2 in class B located in assembly 2, and an instance P of your custom permission.
TIP
If you are using default security policy, your secured assembly X.dll should be granted unrestricted use of your custom permission if X.dll is located on your local machine.
If you want to ensure some assembly is not granted some custom permission state P, then use an assembly permission request like the following:
[assembly:CustomPermission(SecurityAction.RequestRefuse, State=P)]
You will need to modify the State=P portion of this assembly permission request to fit your specific custom permission. See Chapter 6 for more details of assembly permission requests.
A.a() has a LinkDemand for P. Assembly 1 is granted P when it is loaded, and assembly 2 is not granted P when it is loaded. B.b1() calls B.b2(), which calls A.a(). A SecurityException should be thrown inside B.b1() at the point when it calls B.b2().
A has a LinkDemand for P. Assembly 1 is granted P when it is loaded, and assembly 2 is not granted P when it is loaded. B.b1() calls B.b2(), which creates an instance of A and calls A.a(). A SecurityException should be thrown inside B.b1() at the point when it calls B.b2().
A.a() has a LinkDemand for P. Assembly 1 is granted P when it is loaded, and assembly 2 is granted P when it is loaded. B.b1() calls B.b2(), which calls A.a(). No SecurityException should be thrown in any of these calls due to the LinkDemand.
What follows are the key scenarios to check for an InheritanceDemand. All the scenarios will refer to method a in class A located in assembly 1, method b in class B located in assembly 2, methods c1 and c2 in class C in assembly 3, and an instance P of your custom permission. Class B inherits from class A, and method b is an overridden version of method A.a().
A.a() has an InheritanceDemand for P. Assemblies 1 and 3 are granted P when they are loaded, and assembly 2 is not granted P when it is loaded. C.c1() calls C.c2(), and C.c2() creates an instance of B. A SecurityException should be thrown when C.c1() calls C.c2().
A has an InheritanceDemand for P. Assemblies 1 and 3 are granted P when they are loaded, and assembly 2 is not granted P when it is loaded. C.c1() calls C.c2(), and C.c2() creates an instance of B. A SecurityException should be thrown when C.c1() calls C.c2().
A has an InheritanceDemand for P. Assemblies 1, 2, and 3 are granted P when they are loaded. C.c1() calls C.c2(), and C.c2() creates an instance of B and calls B.b(). No SecurityException should be thrown in any of these calls due to the InheritanceDemand.
There are two issues that might be worthwhile investigating while testing a custom permission. The first issue is use of globalized data in the permission. By globalized data, I mean data that is different in different cultures, such as dates and character sets. The second issue is remoting across AppDomains.
If the custom permission you need to test consists only of a group of Boolean flags, skip this section. However, if the permission is more complex and uses strings or dates, it is worthwhile to take note here.
When testing a custom permission, it is important to really examine the whole realm of possible inputs to the permission. If the permission stores state in the form of a string (for example, the FileIOPermission) and that string ever left managed code, there is a chance that there could be a string parsing problem in the unmanaged code. Strings and dates (among other types) are also related to cultures. It is worthwhile to try giving data formatted for other cultures to the permission. A data parsing problem in a permission could easily open up a security hole in the system.
Stack walks will not propagate in remoting across machine boundaries. However, they will propagate within a process across AppDomains. Because of the complexity of remoting and serialization of permissions, it would be useful to at least try a scenario involving a stack walk across AppDomains for your custom permission. If your permission is incorrectly serialized or deserialized across the AppDomain boundary, it could turn out to be a problem if complex applications tried to use your custom permission or any secured assemblies using your custom permission. Listing 26.5 shows an example of stack walking across app domains. Note that this requires multiple assemblies, so the example is broken into two source files.
NOTE
See the .NET Framework SDK documentation for more information on remoting and serialization.
18.188.53.32