Although for the most part administrative security configuration is sufficient, .NET also provides various programmatic
ways to control and enforce security. You can use these powerful
techniques to tighten security, optimize performance, handle unknown
security policies, and deal with questionable components. In addition,
programmatic security can configure security at the component level
(unlike administrative security configuration, which is only as granular
as a single assembly). All the permission
types have corresponding classes and attributes available to you. In
fact, administrative configuration uses these classes indirectly; the
security configuration files are just a list of classes to use when
providing the configurable permissions.
Although system
administrators can grant assemblies permissions by using administrative
configuration, there is no programmatic way to grant permissions. The
reason is clear: if that were possible, a rogue assembly could grant
itself permissions and go about causing harm. Programmatic security can
deny security permissions or demand that some permission be granted. You
can use the permission classes dynamically during runtime or apply them as class or assembly attributes, indicating which security action to take and when.
1. The Permission Classes
Most permission classes are defined in the System.Security.Permissions namespace, but some are spread all over the .NET Framework. All permission classes implement a set of interfaces, including IPermission, which is defined as:
public interface IPermission : ISecurityEncodable
{
IPermission Copy( );
void Demand( );
IPermission Intersect(IPermission target);
bool IsSubsetOf(IPermission target);
IPermission Union(IPermission target);
}
IPermission is defined in the System.Security namespace. The base interface ISecurityEncodable,
used mostly when constructing and persisting custom permissions,
provides methods to convert the permissions to and from XML security
elements.
1.1. Permission demand
The most useful method of IPermission is Demand( ),
which triggers a stack walk demanding the permission of all the callers
up the stack. For example, here is the code required to trigger a stack
walk; it verifies that all the callers up the stack have permission to
write to the C:\Temp directory:
IPermission permission;
string path = @"C:\Temp\";
permission = new FileIOPermission(FileIOPermissionAccess.Write,path);
permission.Demand( ); //Trigger stack walk
If during the stack walk .NET discovers a caller coming from an assembly
without the demanded permission, it aborts the stack walk, and the call
to Demand( ) throws an exception of type SecurityException.
Using Demand( )
is how the various .NET application frameworks classes require that
whoever calls them have the required security permissions. For example,
the file I/O class FileStream demands
permission to perform the appropriate operations (such as opening or
creating files) in its constructors. You can take advantage of Demand( ) to tighten security and optimize performance as well.
For example, demanding permissions is recommended when a component uses a resource on behalf of a client. Consider the StreamWriter
class. It demands file I/O permission when it's constructed with a path
parameter, but subsequent calls on its methods cause no demands. The
reason the designer of the StreamWriter
class decided not to demand the file I/O permission in every call is
because it would have rendered the class useless for performing intense
file I/O operations. For example, if the client writes the entire
Encyclopedia Britannica to the disk one character at a time in a tight
loop, demanding the permission every time would kill the application.
Not demanding the permission may be fine if a component creates a StreamWriter
object and never uses it on behalf of external clients, even
indirectly. But in reality, the reason a component creates a resource is
to use it to service its clients. Thus, if the permission is demanded
only when a component is constructed, a malicious and untrusted client
could simply wait for a trusted client to successfully create the
component and its resources, and then call its resource-accessing
methods.
Unlike the designer of the StreamWriter class, however, you are intimately aware of the particular context in which the StreamWriter
class is used. If the file I/O operations performed are not extreme,
the component can compensate by explicitly demanding the appropriate
permissions, as shown in Example 1.
Example 1. Protecting a resource by demanding access permission
using System.IO; using System.Security; using System.Security.Permissions; public class MyClass { StreamWriter m_Stream; string m_FileName = @"C:\Temp\MyFile.txt"; public MyClass( ) { //The StreamWriter demands permissions here only: m_Stream = new StreamWriter(m_FileName); } public void Save(string text) { //Must demand permission here: IPermission permission; permission = new FileIOPermission(FileIOPermissionAccess.Write,m_FileName); permission.Demand( ); m_Stream.WriteLine(text); } }
|
Another example is an object that is about to perform a lengthy,
intensive calculation and then save it to the disk. Ultimately, if the
callers up the stack don't have the required permission, the file I/O
classes throw an exception when trying to open the file. Instead of
wasting time and resources performing a calculation that can't
eventually be saved, the object can first demand the permission and then
proceed with the calculation only if it's certain it can persist the
results. Example 2 demonstrates this technique.
Example 2. Optimizing by demanding permission before performing the operation
class MyClass { public void Calclulate(string resultsFileName) { IPermission permission; permission = new FileIOPermission(FileIOPermissionAccess.Write, resultsFileName); try { permission.Demand( ); } catch(SecurityException exception) { string message = exception.Message; message += ": Caller does not have permission to save results"; MessageBox.Show(message); return; } // Perform calculation here and save results DoWork( ); SaveResults(resultsFileName); } //Helper methods: void DoWork( ) {...} void SaveResults(string resultsFileName) {...} }
|
Note that calling Demand( ) verifies only that the callers up the call chain have the requested security permission. If the object calling Demand( )
itself doesn't have permission, when it tries to access the resource or
perform the operation, the resource (or operation) may still throw a
security exception.
1.2. Permission interactions
Some permissions imply
other permissions. For example, file I/O access permission to a folder
implies access permission to individual files in the same folder. The IPermission interface provides a number of methods that examine how two different permissions relate to each other. The IsSubsetOf( ) method returns true if a specified permission is a subset of the current permission object:
IPermission permission1;
IPermission permission2;
string path1 = @"C:\temp\";
string path2 = @"C:\temp\MyFile.txt";
permission1 = new FileIOPermission(FileIOPermissionAccess.AllAccess,path1);
permission2 = new FileIOPermission(FileIOPermissionAccess.Write,path2);
Debug.Assert(permission2.IsSubsetOf(permission1));
The Intersect( ) method of IPermission
returns a new permission object that is the intersection of the
original permission object and the specified permission object, and the Union( )
method returns a new permission object equivalent to the union of the
two. .NET uses these methods when calculating the union of permissions
granted by the evaluated code groups in a policy and when calculating
the intersection of the policies.
In the rare case of
components using a resource type (such as a hardware item) not protected
by any of the built-in permissions, you can provide custom
permission classes. The custom permission typically grants access to
the resource only, instead of to a communication mechanism (such as a
communication port or the filesystem) that can access other resources.
All .NET permission classes derive from the abstract class CodeAccessPermission, which provides the implementation of the stack walk. You can start creating a custom permission by deriving from CodeAccessPermission.
Custom permission classes can be used both programmatically and
administratively (via custom XML representation). The assembly
containing the custom permissions must be strongly named and deployed in
the GAC. In addition, it must be granted full trust, and .NET must be
aware of it. To register the assembly with .NET, use the .NET
Configuration tool. Each policy has a folder called Policy Assemblies.
Select Add from the folder's context menu, then select the custom
permission assembly from the list of assemblies in the GAC. Note that
you must deploy the custom permission assembly and register it on every
machine that has applications using it.
|