Implementing Your Own Permissions

for RuBoard

From time to time, it will be necessary to implement your own custom permissions. This will most likely come about because your code has created a new form of resource to which access must be policed.

For example, you might be controlling access to some new external hardware device. For the purposes of illustration, we're going to assume the device is a toaster. The managed code that will provide access to the toaster needs to provide methods to determine the presence of bread, its current "toasted" level, eject the bread, and so on.

Listing 25.1 is a simplified model of the Toaster class, along with some possible security declarations we may use to control access to the toaster resource.

Listing 25.1 Outline of the Toaster Class
 // Enum describing the level of toasting of a piece of bread. public enum ToastLevel {     Uncooked,     LightBrown,     MediumBrown,     DarkBrown,     Burnt } public class Toaster {     // Constructing a toaster object implies gaining exclusive     // access to the actual toaster hardware (in a real example,     // we'd write a finalizer method to deal with releasing the     // hardware resources when we're finished with the managed     // object). A more complex example might allow the toaster     // to be accessed in a read-only mode, in which case the     // permission required on the constructor could be dynamically     // computed, and we'd move from declarative to imperative     // security.     [ToasterPermission(SecurityAction.Demand, ExclusiveAccess=true)]     public Toaster()     {     }     // Read-only property telling us whether any bread is currently     // in the toaster.     [ToasterPermission(SecurityAction.Demand, Information=true)]     public bool ContainsBread     {         get { }     }     // Read-only property telling us the current cooked status of     // the bread in the toaster.     [ToasterPermission(SecurityAction.Demand, Information=true)]     public ToastLevel Toastedness     {         get { }     }     // Method to eject the bread from the toaster.     [ToasterPermission(SecurityAction.Demand, Eject=true)]     public void EjectBread()     {     }     // Method to eject the bread from the toaster once it has reached     // the supplied level of toastedness.     public void EjectBread(ToastLevel level)     {         // Need both the Eject permission and the right level of         // Toastedness permission (not everyone is allowed to produce         // burnt toast).         ToastedPermission tp;         tp = new ToastedPermission(PermissionState.None);         tp.Eject = true;         tp.ToastLevel = level;         tp.Demand();     } } 

This example is obviously rather contrived, but it demonstrates the basics. We'll be using it as the basis for much of our discussions in the following sections.

An important point to note at this stage is that the .NET Framework (at least in Version 1) does not support extension of the security system from partial trust code. That is, to create our new permission, the assembly that provides the implementation must be granted full trust. Of course, this does not restrict which assemblies can use or be granted the new permission. What this means in practice is that you should code new permissions expecting them to be installed onto the user 's machine as opposed to an Internet downloadable control or a network shared component.

We're going to implement a code access security (CAS) permission for our toaster project. This is the most common type of .NET Framework permission (all the permissions we've used so far in this chapter are CAS). Code access security permissions are simply those that are granted based on the identity of the assembly and, as such, are granted on a per-assembly basis. They're the ones the runtime checks by (typically) walking the stack and finding the assemblies for each method in the call chain.

The .NET Framework also supports non-code access security permissions (non-CAS). The implementation of individual non-CAS permissions defines how and to whom such permissions are granted. For example, the .NET Framework defines role-based permissions that record role identities at a thread level. When a demand for one of these permissions occurs, the implementation of the demand looks at data stored in thread local storage to determine the result, rather than walking the stack.

From the implementation point of view, the difference between CAS and non-CAS permissions is simply down to a matter of inheriting from the right class. All permission types need to implement the IPermission interface; this defines the methods that the runtime uses to interact with the permission internally, as well as the Demand method with which we're familiar. In addition, CAS permissions need to derive from System.Security.CodeAccessPermission . This provides the implementation for IPermission.Demand , handling all of the stack walking aspects. CodeAccessPermission 's implementation of Demand will delegate the actual work of comparing permission instances and so on to the other methods of IPermission (for which you provide the implementation).

CodeAccessPermission also provides methods such as Assert and Deny ; these are stack walking operations and, as such, make no sense for non-CAS permissions and are not included in IPermission .

So let's start a skeleton implementation of our ToasterPermission , presented in Listing 25.2.

Listing 25.2 Basic Outline of the ToasterPermission Class ” ToasterPermission.cs
 [Serializable] sealed public class ToasterPermission : CodeAccessPermission,                                         IUnrestrictedPermission {     // Permission state fields. Normally we'd make these private     // and provide public property accessors, but for simplicity's     // sake we'll just make them public (none are read-only anyway).     // The ability to exclusively control the toaster.     public bool ExclusiveAccess;     // The ability to read toast state information.     public bool Information;     // The ability to eject toast.     public bool Eject;     // The maximum level to which auto-eject can be set for the     // toaster.     public ToastLevel ToastLevel;     // Provide the usual constructor taking a PermissionState (for     // consistency with other permission types).     public ToasterPermission(PermissionState state)     {         if (state == PermissionState.Unrestricted)         {             ExclusiveAccess = true;             Information = true;             Eject = true;             ToastLevel = ToastLevel.Burnt;         }     } } 

NOTE

Beginning with Listing 25.2, we'll be building up a fully functional permission class. The full source code for the finished class can be downloaded as ToasterPermission.cs from the publisher's Web site.


The Serializable attribute marks this class as serializable via the standard .NET Framework binary serializer. This is a requirement when implementing custom permissions (the runtime internally serializes/deserializes permission instances in a number of cases). For most permission types, simply supplying the Serializable attribute is sufficient; the binary serializer will automatically determine the correct way to serialize your data. If your state data has special requirements, merely making direct copies of the fields won't capture the correct state; you may implement the ISerializable interface to better control the process. This topic is outside the scope of this book.

We seal the class because there is no real reason to allow anyone to derive from us, and sealing will prevent the possibility of any derivation-based security attacks. This is a good practice in general, and it's not confined to the implementation of permission classes.

We derive from CodeAccessPermission because we're implementing a CAS style permission. CodeAccessPermission implements Ipermission , so we don't explicitly reimplement it here (although we will need to implement some of the IPermission methods that CodeAccessPermission doesn't provide for us).

We implement IUnrestrictedPermission to indicate that this permission is considered part of the unrestricted permission set (that is, part of full trust and not an identity permission). If we wanted to ensure that ToasterPermission is never to be granted automatically and unthinkingly by the security policy system (that is, it would have to be granted with an explicit rule in the policy database), we could skip the implementation of IUnrestrictedPermission . We discuss this interface later in this section.

We provide the usual constructor (taking a PermissionState as argument) for consistency with other permission types. We could also provide alternative constructors to set the state to varying levels of access, but this permission is simple enough that this is not really a requirement.

So far, all we have in the body of ToasterPermission are the state fields necessary to indicate the levels of access we determined to be meaningful earlier. We now need to implement the methods the runtime security system will use to manipulate and query an instance. Let's look at the definition of IPermission first:

 public interface IPermission : ISecurityEncodable {     IPermission Copy();     IPermission Intersect(IPermission target);     IPermission Union(IPermission target);     bool IsSubsetOf(IPermission target);     void Demand(); } 

The IPermission interface itself implements another interface ” ISecurityEncodeable . Ignore this for now; we'll explain it in detail later in this section.

The Copy method simply creates a clone of the current permission instance and returns it. This is necessary because several .NET Framework runtime security APIs return permission instances from the assembly grant set; copies must be returned or the caller can simply alter the permission and thereby modify the grant set itself. For that reason, the implementation of Copy should perform a deep copy ”that is, any permission state data that is itself an object reference should be recursively cloned as well.

This is a simple method for us to implement, given that our state data is not all that complex. We present a possible implementation in Listing 25.3.

Listing 25.3 Implementing IPermission.Copy
 public override IPermission Copy() {     ToasterPermission perm;     // Create a vanilla permission with no state.     perm = new ToasterPermission(PermissionState.None);     // Copy state fields from the current instance into the     // new instance.     perm.ExclusiveAccess = ExclusiveAccess;     perm.Information = Information;     perm.Eject = Eject;     perm.ToastLevel = ToastLevel;     // Return the new copy.     return perm; } 

The Intersect method returns a permission that is the result of computing set intersection between the current permission and the one provided as an argument. That is, the permission returned will represent all the common state between the two input instances. If there is no common state, null can be returned (rather than a permission instance with no state set). It is not absolutely necessary to return null in these circumstances, but doing so can lead to performance improvements during evaluation of demands.

The exact definition of common state is up to the permission type to decide (the definition should obviously give deterministic, intuitive results that a user or system administrator can understand).

For ToasterPermission , it's easy to define the meaning of common state for our Boolean state fields; if both permission instances have the same field set to true , that aspect of the permission state is shared (this works because the various state fields are totally independent of each other; one can imagine a more complex permission where the algorithm would have to take into account subtle interactions between the fields).

The ToastLevel state field is not quite so straightforward; given two values of the enumeration, how do we compute a third value that represents the common aspects of the two? The approach here is to consider the fact that we deem the enumeration to be an ordered list. That it is the ability to set the auto-eject level to Burnt is considered a more privileged operation than setting it to Uncooked . In such scenarios, the common state is the highest level of access that both permissions are granted. For ToastLevel , this is simply the lower value of the two enumerations (by default, C# will number enumerations from 0 in ascending order).

Listing 25.4 gives a possible implementation of Intersect .

Listing 25.4 Implementing IPermission.Intersect
 public override IPermission Intersect(IPermission target) {     // An input of null is a special case (it represents a     // permission with no state). There is obviously no common     // state in such cases and we should return null to represent     // this.     if (target == null)         return null;     // Cast target into a ToasterPermission for ease of use.     // Note that this will throw an InvalidCastException if     // someone mistakenly passes us the wrong permission instance.     ToasterPermission other = (ToasterPermission)target;     ToasterPermission result;     // Create a vanilla permission with no state.     result = new ToasterPermission(PermissionState.None);     // Consider shared state for each state field in turn.     result.ExclusiveAccess = ExclusiveAccess && other.ExclusiveAccess;     result.Information = Information && other.Information;     result.Eject = Eject && other.Eject;     // Shared state for ToastLevel is the lesser of the two.     result.ToastLevel = ToastLevel < other.ToastLevel ?         ToastLevel : other.ToastLevel;     // Return the shared state instance.     return result; } 

The next method to consider is Union . This is similar to Intersect but computes the permission instance containing all state from both inputs. It can be thought of as adding two permissions together. When we compute the union of the ToastLevel state fields, we will pick the most permissive (highest value) of the pair.

Our implementation is given in Listing 25.5.

Listing 25.5 Implementing IPermission.Union
 public override IPermission Union(IPermission target) {     // An input of null is a special case (it represents a     // permission with no state). We can just return a copy of     // the current permission in this case.     if (target == null)         return Copy();     // Cast target into a ToasterPermission for ease of use.     // Note that this will throw an InvalidCastException if     // someone mistakenly passes us the wrong permission instance.     ToasterPermission other = (ToasterPermission)target;     ToasterPermission result;     // Create a vanilla permission with no state.     result = new ToasterPermission(PermissionState.None);     // Consider shared state for each state field in turn.     result.ExclusiveAccess = ExclusiveAccess  other.ExclusiveAccess;     result.Information = Information  other.Information;     result.Eject = Eject  other.Eject;     // The union of state for ToastLevel is the most permissive     // of the two.     result.ToastLevel = ToastLevel < other.ToastLevel ?                         other.ToastLevel : ToastLevel;     // Return the shared state instance.     return result; } 

IsSubsetOf returns a Boolean result indicating whether the current permission is a subset of the permission given as an argument. To be a subset, every piece of state in the current permission must also be present in the target permission. Where we have state such as ToastLevel , the current permission should have a ToastLevel less than or equal to the target permission. (Note that equality part; a permission is considered to be a subset of another if both are identical).

IsSubsetOf is the principal means that CodeAccessPermission uses to determine if a given assembly grant set satisfies a demand permission set. If every permission in the demand permission set is a subset of the corresponding grant permission, the demand set can be considered granted.

Listing 25.6 gives a possible implementation.

Listing 25.6 Implementing IPermission.IsSubsetOf
 public override bool IsSubsetOf(IPermission target) {     // An input of null is a special case (it represents a     // permission with no state). We can only be a subset if     // we're in a similar empty state.     if (target == null)         return !ExclusiveAccess &&                !Information &&                !Eject &&                ToastLevel == ToastLevel.Uncooked;     // Cast target into a ToasterPermission for ease of use.     // Note that this will throw an InvalidCastException if     // someone mistakenly passes us the wrong permission instance.     ToasterPermission other = (ToasterPermission)target;     // For each state field that's in a non-empty state, check     // that the state is mirrored (or present at a greater     // permissive state) in the target.     if (ExclusiveAccess && !other.ExclusiveAccess)         return false;     if (Information && !other.Information)         return false;     if (Eject && !other.Eject)        return false;     if (ToastLevel > other.ToastLevel)        return false;     // Checked all state, we must be a subset.     return true; } 

There is no need for us to provide an implementation of Demand , the last method in IPermission . CodeAccessPermission has already provided an implementation for us (that knows how to walk the stack and call back into our implementation of IsSubsetOf for each assembly grant set).

However, if we were implementing a non-CAS permission and, therefore, did need to provide an implementation of Demand , we would very likely use something like the code presented in Listing 25.7.

Listing 25.7 Implementing IPermission.Demand
 public override void Demand() {     // Use permission specific means to determine what permission     // state is granted. In role based security for instance, the     // current thread would be queried for the account of the user     // in control. This information could then be mapped to the     // permission state granted to that user.     MyPermission granted = GetGrantedState();     // Check that the demand made is a subset of what's been granted.     // If not we inform the security system by throwing a security     // exception.     if (!IsSubsetOf(granted))         throw new SecurityException("Insufficient permissions",                                     typeof(MyPermission),                                     ToXml().ToString()); } 

Note that when throwing a security exception from Demand (to indicate insufficient granted permissions to satisfy the request), the SecurityException constructor may be given additional parameters to help the user diagnose security failures. The first parameter is a string specifying the exception message (you may want to make this string localized for various cultures in your implementation, perhaps by using the .NET Framework resource manager facility). The second parameter is the System.Type of the demand (in our example, we assumed this was a permission type called MyPermission ). The third and final parameter is a string, which by convention contains the XML serialized state of the demand (we'll see the ToXml method again later). These pieces of information can be retrieved from the exception either through the use of ToString (in which case the additional data is merged in which the usual stack trace information) or individually through the Message , PermissionType , and PermissionState properties, respectively.

Getting back to our toaster example, you'll recall that ToasterPermission also claimed to implement the IUnrestrictedPermission interface. This is a very simple interface:

 public interface IUnrestrictedPermission {     bool IsUnrestricted(); } 

The sole method, IsUnrestricted , should return true if the permission instance is in the unrestricted state (that is, the most permissive state it can be in) and false at all other times. For ToasterPermission , this equates to all of our Boolean state fields being set and ToasterLevel being set to its highest state ” Burnt .

Listing 25.8 gives an implementation of IsUnrestricted .

Listing 25.8 Implementing IUnrestrictedPermission.IsUnrestricted
 public bool IsUnrestricted() {     return ExclusiveAccess &&            Information &&            Eject &&            ToastLevel == ToastLevel.Burnt; } 

We still have a couple methods to implement; remember the ISecurityEncodable interface implicitly brought in by IPermission ?

 public interface ISecurityEncodable {     SecurityElement ToXml();     void FromXml(SecurityElement e); } 

The purpose of these methods is to translate instances of your permission back and forth between an XML serialized format used to persist the permissions to disk. For example, declarative security stored in the metadata of an assembly is stored in this format.

Rather than translating directly from the permission instance to a string (or vice versa), an intermediate form is used ” System.Security.SecurityElement . This structure represents a single node that can be linked into a tree structure (each node has a single parent and zero or more children, with the exception of the root node, which has no parent).

Let's consider the following piece of XML:

 <Foo bar="1" baz="true">Hello World<SubFoo/></Foo> 

This can be represented by two SecurityElements . The top level SecurityElement has the following values associated with it:

  • A tag ”in this case, the string "Foo" .

  • Zero or more attributes ”in this case, "bar" and "baz" . Each attribute has a string value; "1" and "true" in the example, respectively.

  • An optional text string ” "Hello World" , in this case.

  • Zero or more child SecurityElement s ”in this case, we have one child (tag "SubFoo" , no attributes, text, or children of its own).

Given the following declarative description of permission state

 [FileIOPermission(SecurityAction.Demand,                   Read=@"c:\ foo.dat",                   Write=@"c:\ bar.dat")] 

the XML produced would be as follows :

 <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=1.0. graphics/ccc.gif 3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"                 version="1"                 Read="c:\ foo.dat"                 Write="c:\ bar.dat"/> 

Some parts of this encoding are dictated by the .NET Framework runtime ”the tag of IPermission , the attributes class (set to the assembly qualified type name of your permission), and version (set to "1" ). The other attributes ( Read and Write in this case) are defined by the permission type itself. Note that, by convention, these attributes are capitalized to distinguish them from the runtime attributes.

You'll notice that no text or child elements are used; permissions rarely have any truly complex state to encode, and attributes usually suffice.

Regardless of how you choose to encode your permission state, SecurityElement provides a slew of methods to create nodes, manipulate attributes and text, and link children. The following is a simplified view of the SecurityElement class:

 public class SecurityElement {     // Construct a SecurityElement with no associated text.     public SecurityElement(String tag) { }     // Construct a SecurityElement with the specified text.     public SecurityElement(String tag, String text) { }     // Get or set the tag name.     public String Tag {  get { }  set { }  }     // Get or set a hash table of the element attributes. Keys are the     // attribute names, values are the attribute values (always a     // string).     public System.Collections.Hashtable Attributes {  get { }  set { }  }     // Get or set the text for an element.     public String Text {  get { }  set { }  }     // Get an array list of the children of the current element.     public ArrayList Children {  get { }  set { }  }     // Add an attribute with the given name and value.     public void AddAttribute(String name, String value) { }     // Add a child SecurityElement.     public void AddChild(SecurityElement child) { }     // Compare elements for equality; tags, text, attributes and     // children must all match to be considered equal.     public bool Equal(SecurityElement other) { }     // Check whether a given tag string is legal.     // To be valid, tag must be non-null and contain none of the     // following characters: ' ', '<', '>'.     public static bool IsValidTag(String tag) { }     // Check whether a given text string is legal.     // To be valid, text must be non-null and contain none of the     // following characters: '<', '>'.     public static bool IsValidText(String text) { }     // Check whether a given attribute name is legal.     // To be valid, name must be non-null and contain none of the     // following characters: ' ', '<', '>'.     public static bool IsValidAttributeName(String name) { }     // Check whether a given attribute value is legal.     // To be valid, value must be non-null and contain none of the     // following characters: '<', '>', ""'.     public static bool IsValidAttributeValue(String value) { }     // Escape invalid characters ('<', '>', '"', ''', '&') in     // attribute values or text. Such values will always be returned     // unescaped, therefore no Unescape method is exported.     public static String Escape(String str) { }     // Return the string (XML) form of the element.     public override String ToString () { }     // Return the value for the named attribute.     public String Attribute(String name) { }     // Search for a child element by tag name.     public SecurityElement SearchForChildByTag(String tag) { }     // Search this element and all children (recursively) for the     // named tag. If found, return the associated text.     public String SearchForTextOfTag(String tag) { } } 

ToasterPermission 's implementation of the ISecurityEncodeable might look something like the code in Listing 25.9.

Listing 25.9 Implementing ISecurityEncodable Methods
 // Convert the current permission to a SecurityElement (or tree // of elements). public override SecurityElement ToXml() {     // Create new element, tag name must always be "IPermission",     // element text is not needed.     SecurityElement elem = new SecurityElement("IPermission");     // Determine assembly qualified full type name of our permission     // class (the security system uses this to locate and load the     // class).     String name = typeof(ToasterPermission).AssemblyQualifiedName;     // Add attributes for the class name and protocol version     // (currently always "1") to the element.     elem.AddAttribute("class", name);     elem.AddAttribute("version", "1");     if (IsUnrestricted())     {         // Create an abbreviated encoding for unrestricted         // instances of this permission.         elem.AddAttribute("Unrestricted", Boolean.TrueString);     }     else     {         // Encode each state field as an attribute of the element.         // Only bother adding elements for state in the non-default         // state, this makes the encoded format more compact.         if (ExclusiveAccess)             elem.AddAttribute("ExclusiveAccess", Boolean.TrueString);         if (Information)             elem.AddAttribute("Information", Boolean.TrueString);         if (Eject)             elem.AddAttribute("Eject", Boolean.TrueString);         if (ToastLevel != ToastLevel.Uncooked)             elem.AddAttribute("ToastLevel", ToastLevel.ToString());     }     // Return the completed element.     return elem; } // Convert a SecurityElement (or tree of elements) to a permission // instance. public override void FromXml(SecurityElement e) {     // Check whether we have an unrestricted instance.     String unrestricted = e.Attribute("Unrestricted");     if (unrestricted != null &&         Boolean.Parse(unrestricted))     {         ExclusiveAccess = true;         Information = true;         Eject = true;         ToastLevel = ToastLevel.Burnt;         return;     }     // Ensure we start from a clean base state.     ExclusiveAccess = false;     Information = false;     Eject = false;     ToastLevel = ToastLevel.Uncooked;     // Check the value of each attribute which encodes a state     // field.     String value;     value = e.Attribute("ExclusiveAccess");     if (value != null)         ExclusiveAccess = Boolean.Parse(value);     value = e.Attribute("Information");     if (value != null)         Information = Boolean.Parse(value);     value = e.Attribute("Eject");     if (value != null)         Eject = Boolean.Parse(value);     value = e.Attribute("ToastLevel");     if (value != null)         ToastLevel = (ToastLevel)Enum.Parse(typeof(ToastLevel),                                             value, true); } 

There are a number of interesting points to note about the implementation just given:

  • We optimize the encoding of the unrestricted state to a single attribute ” "Unrestricted" = "true" . This is convention followed by all the built-in .NET Framework runtime permissions and helps keep the encoding compact.

  • Another method of keeping the encoding compact is to add attributes only for state fields that are in their non-default state. (When decoding the permission again, it's important to remember to clear any existing state in the permission instance back to its default state for this to work.) Also note that the FromXml code is robust in that it takes the value of the attribute into consideration (not just the existence of the attribute), so adding an attribute, such as Information=false , to the XML will have the expected effect. Even though we never encode state in this fashion in ToXml , this will make the XML form of the permission easier to edit by hand.

  • We make extensive use of the ToString and Parse methods provided by runtime primitive types to encode and decode state field values. This simplifies the code and allows for flexibility in input formats (such as case insensitivity) which, again, can ease editing of the XML by hand.

That should complete our implementation of ToasterPermission . At this point, it should be possible to compile the permission and try a few simple tests (constructing instances with different state, encoding them to XML and checking the results by hand, and so on). It should also be possible to demand permission instances (because we implemented IUnrestrictedPermission , ToasterPermission will be granted to assemblies with full trust, but no other assemblies).

To use the new permission declaratively , we'll need to create a custom attribute class for it.

Implementing a Security Custom Attribute

The .NET Framework runtime uses a custom attribute class to encode permission instances for a couple reasons. First, it can leverage the existing syntax, compiler support, and user understanding of custom attributes. Second, the extra level of indirection (the custom attribute code itself) allows the permission author a great deal of flexibility. This second point needs some explanation.

Although the syntax of a security custom attribute mirrors that of any other custom attribute, they're treated somewhat differently by the compiler and the runtime. Whereas a standard custom attribute is encoded directly into the assembly metadata at compilation time, a security custom attribute is actually instantiated (that is, the runtime is started up and an actual instance of the custom attribute is created). A special method on the security custom attribute is then called to create a permission instance. It is this permission instance that is serialized (via the ToXml method we looked at earlier) and placed into the metadata.

This additional step (creating a security custom attribute instance during compilation) allows the attribute author to perform non-trivial processing at compile time.

For example, public key information could be extracted from a certificate file and placed in the resulting permission.

For more trivial permissions (such as our ToasterPermission ), the implementation of the attribute class is more straightforward. The attribute serves as a container for all the permission state data, which is then used to re-create the desired permission instance.

One very important restriction is imposed on permissions that are to be used declaratively (as a result of the use of security custom attribute classes and their compile time instantiation). No permission (or permission attribute) class can be used in the same assembly in which it is defined. Otherwise, the runtime will attempt to load and use an assembly that the compiler has not finished emitting yet, with unpredictable results. Commonly, the permission and associated permission attribute class are defined in a separate assembly of their own, away from all the other code in the project.

The runtime locates the permission assembly in the same manner it would any other assembly (which is different from the search rules used by the compiler to locate references). So, when compiling an assembly that uses one of your new permissions declaratively, the permission assembly should be either placed in the current directory (easy for quick and dirty testing of new permissions) or installed into the Global Assembly Cache (GAC).

The code present in the security custom attribute implementation will be granted permissions in the same manner as any other assembly. Because the assembly will usually be loaded from the local computer, the assembly will typically be granted full trust. Because this might be undesirable in certain circumstances (if source code is being automatically generated from instructions from an untrusted source, for example), there is a mechanism to lock down permissions for security custom attribute code. If the environment variable _ClrRestrictSecAttributes is defined to a nonempty value, and then all code run during a compilation will be restricted to ExecutionPermission only. For example

 set _ClrRestrictSecAttributes=1 csc TestPermission.cs 

Implementing a security custom attribute is relatively simple; an attribute class is created in much the same way as for a standard custom attribute (we'll see an example soon). Security custom attributes derive from one of two security classes ( System.Security.Permissions.SecurityAttribute or System.Security.Permissions.CodeAccessSecurityAttribute ) rather than System.Attribute . The intent was that attributes for CAS permissions derive from CodeAccessSecurityAttribute and non-CAS from SecurityAttribute . However, due to a bug in compiler support, inheriting your non-CAS permission attributes from SecurityAttribute may not work in all languages. For V1 of the .NET Framework, it's recommended that all security attributes derive from CodeAccessSecurityAttribute . This may look odd, but will not have any impact on the functionality of your attributes.

There are three main components of a security custom attribute:

  • A single constructor that takes a SecurityAction as an argument and simply delegates to the base constructor (you can also implement any other initialization code here as well if you want).

  • A set of fields and/or properties that the caller can use to set state in the permission. These should closely mirror those in the permission implementation itself (down to having the same name, if possible).

  • A method called CreatePermission . It is this method that the runtime will call to create the fully formed permission. The implementation will typically use the information stored in the previously mentioned fields to set the state in the final permission object.

During compilation of a declarative security attribute based on your permission, the runtime will create an instance of your attribute class ( causing the constructor to run), set all the fields or properties referenced in the declarative security statement, and call the CreatePermission method. Note that the SecurityAction passed to the constructor is not actually intended for use by the permission attribute or permission ”the constructor argument was just a convenient place to record it (from a custom attribute syntax point of view). In actuality, the runtime parses out the action prior to even calling the attribute constructor and stores it in the assembly metadata separately.

Let's look at the implementation of a security attribute to complement our ToasterPermission , given in Listing 25.10.

Listing 25.10 Implementing a PermissionAttribute Class ” ToasterPermissionAttribute.cs
 [Serializable,  AttributeUsage(AttributeTargets.Method                  AttributeTargets.Constructor                  AttributeTargets.Class                  AttributeTargets.Struct                  AttributeTargets.Assembly,                 AllowMultiple = true,                 Inherited = false)] sealed public class ToasterPermissionAttribute : CodeAccessSecurityAttribute {     // State fields to mirror those in actual permission.     public bool ExclusiveAccess;     public bool Information;     public bool Eject;     public ToastLevel ToastLevel;     // Nothing to do in constructor but pass action code back up to     // the base implementation.     public ToasterPermissionAttribute(SecurityAction action)         : base(action)     {     }     // Create a permission instance with the current state set in the     // attribute.     public override IPermission CreatePermission()     {         ToasterPermission perm;         // The runtime automatically provides a property which         // indicates whether an unrestricted instance is required.         if (Unrestricted)             return new ToasterPermission(PermissionState.Unrestricted);         // Create an instance with default (empty) state.         perm = new ToasterPermission(PermissionState.None);         // Copy state across.         perm.ExclusiveAccess = ExclusiveAccess;         perm.Information = Information;         perm.Eject = Eject;         perm.ToastLevel = ToastLevel;         // Return the finished permission.         return perm;     } } 

NOTE

The code for the ToasterPermissionAttribute class given in Listing 25.10 is available for download from the publisher's Web site as ToasterPermissionAttribute.cs .


As for permission classes, security attribute classes should be marked as serializable and public. It's also a good practice to seal them; there is no real reason to allow anyone to subclass this type.

The AttributeUsage attribute is a standard feature of all custom attributes (whether security or standard). Its primary role here is to inform the compiler where use of the attribute is legal (all the places we'd expect for declarative security ”methods, classes, value types, and assemblies). It also notes that multiple instances of the same attribute can appear on a single method, class, and so on (which is obviously important for declarative security) and that the attributes are not inherited through the class hierarchy (that is, an attribute on a base class doesn't imply that same attribute on any derived class). Note that the AttributeUsage annotation is used only by the compiler; it is ignored by the runtime. Therefore, widening the list of attribute targets or marking the attribute as inherited will not enable any extended functionality (for example, making fields a valid attribute target will not enable declarative security on fields).

We provide state fields in the attribute class that mirror the permission class. This is sensible in that it hides the distinction between the two classes to users of your permission (users have no need to understand the complexities of the actual implementation). This illusion is heightened by the fact that the C# compiler will allow the Attribute portion of the custom attribute name to be omitted if no ambiguity results (it is not confused by the presence of a real class with the same name ”that is, the permission class itself ”because the permission class does not derive from System.Attribute and is not considered a viable alternative).

The implementation of CreatePermission is straightforward and merely consists of taking the state data the user set and constructing a matching permission object from it. If you encounter an error at this point, simply throw an exception; the compiler should handle this by displaying the exception name and any exception message embedded within a compiler error message.

You should now be in a position to test your permission declaratively. First, build your permission and permission attribute classes into a single assembly. Next, create a test assembly referencing your permission assembly and containing declarative security statements. After compiling the test assembly, use the PermView command to examine and check the declarative security embedded in the assembly.

 PermView /decl TestAssembly.exe 

Alternatively, you can add Console.WriteLine statements within your implementation of the custom attribute and observe the progress of the various method calls during the compilation process. Remember to remove these output statements from the final product (C#'s conditional compilation feature can be useful here).

for RuBoard


. NET Framework Security
.NET Framework Security
ISBN: 067232184X
EAN: 2147483647
Year: 2000
Pages: 235

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