5. Adding Permissions
The permissions granted or denied by NT security present one level of security; the permissions provided by the classes in the System.Security.Permissions namespace present a second level of security. This second level provides
fine-grained control over specific kinds of tasks, rather than the
coarse general control provided by NT security. For example, the FileDialogPermission class controls how a user can interact with a file dialog box — a fine level of control not provided by NT security.
In many cases, you have a choice between imperative or declarative security in implementing permissions. For example, the FileDialogPermission class provides a means of controlling file dialogs using imperative security, while the FileDialogPermissionAttribute
class provides the same level of control using declarative security.
The choice between imperative security and declarative security is a
matter of personal choice and level of desired control. Declarative
security, which relies on attributes, generally affects an entire
assembly, class, or method, while imperative security works at the code
level. You use imperative security when you need the utmost control of
the permissions and don't mind writing a little more code to get it.
Many developers have gotten used to setting policies
using CAS. Unfortunately, Microsoft has removed this capability in the
.NET Framework 4. Consequently, methods you've used in the past, such
as FileIOPermission.Deny(), no longer work without adding the <NetFx40_LegacySecurityPolicy enabled="true"/> tag to your application . You can see the <NetFx40_LegacySecurityPolicy enabled="true"/> tag documentation at http://msdn.microsoft.com/library/dd409253.aspx. The change makes sense. Virus code could simply use FileIOPermission.Assert() to override any denied privilege created by a lower level. You can read about this update at http://msdn.microsoft.com/library/ee191568.aspx.
You should understand that setting permissions is still important, and that you may need to use the outdated methods, such as FileIOPermission.Deny(),
on some occasions. The CAS Policy example described in the following
sections demonstrates a few important permissions features. First,
you'll see how to use both declarative and imperative security. Second,
the example demonstrates the dangerous nature of using these older
permissions in some environments.
NOTE
One way to reduce the risk of using permissions
to set policy is to sign your executable. Signing the executable makes
it tough for someone to modify your code without the Common Language
Runtime (CLR) detecting the change. This means that denying a
permission will actually deny it as intended as long as your code is
defined correctly.
5.1. Configuring the CAS Policy Example
The CAS Policy example begins with a Windows Forms application. You add a single button called Test (btnTest). In addition, you must add the following using statements to the application:
using System.IO;
using System.Security;
using System.Security.Permissions;
5.2. Creating a Legacy Security Configuration
Because this application is using outdated Deny()
methods, you must also add a configuration file to the application that
allows the use of legacy security. The following steps describe how to
perform this task:
Right-click the project entry in Solution Explorer and choose Add => New Item from the Context menu. You'll see the Add New Item dialog box shown in Figure 2.
Highlight the Application Configuration File template.
Type CAS Policy.EXE.CONFIG in the Name field and click Add. Visual Studio adds the configuration file to your application and opens it for editing.
Type the following code within the <configuration> element:
<runtime>
<NetFx40_LegacySecurityPolicy enabled="true"/>
</runtime>
Highlight the configuration file in Solution Explorer.
Choose Copy if Newer in the Copy to Output Directory property in the Properties window.
5.3. Creating the CAS Policy Example Code
As previously mentioned, imperative security buries
the security checks within the code, which means that you can perform
the checks as needed. Listing 1 shows an example of imperative security.
Example 1. Using imperative security to control file access
// Use declarative security to restrict access to the // test file. [FileIOPermission(SecurityAction.Deny, AllLocalFiles=FileIOPermissionAccess.Read)] private void btnTest_Click(object sender, EventArgs e) { // Call the initial method for opening the file. UseSecurity();
// Call the override method to try again. OverrideSecurity();
} private void UseSecurity() { Stream FS = null; // A test file stream.
// Try to access the object. try { FS = new FileStream( Application.StartupPath + @"\Temp.txt", FileMode.Open, FileAccess.Read); } catch (SecurityException SE) { // Display an error message if unsuccessful. MessageBox.Show("Access Denied\r\n" + SE.Message, "File IO Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; }
// Display a success message. MessageBox.Show("File is open!", "File IO Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
// Close the file if opened. FS.Close(); }
// This method uses imperative security to override the // previous permission denial. private void OverrideSecurity() { FileIOPermission FIOP; // Permission object. Stream FS = null; // A test file stream.
// Create the permission object. FIOP = new FileIOPermission( FileIOPermissionAccess.Read, Application.StartupPath + @"\Temp.txt");
// Allow access to the resource. FIOP.Assert();
// Try to access the object. try { FS = new FileStream(
Application.StartupPath + @"\Temp.txt", FileMode.Open, FileAccess.Read); } catch (SecurityException SE) { // Display an error message if unsuccessful. MessageBox.Show("Access Denied\r\n" + SE.Message, "File IO Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; }
// Display a success message. MessageBox.Show("File is open!", "File IO Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
// Close the file if opened. FS.Close(); }
|
The example begins with a declarative permission attribute, FileIOPermission. In this case, the SecurityAction.Deny value denies a permission, AllLocalFiles defines it as pertaining to all local files, and FileIOPermissionAccess.Read specifies that the read permission is denied. This declarative permission applies to the btnTest_Click() method and every method that btnTest_Click() calls.
To test out the permission, btnTest_Click() calls UseSecurity(). The UseSecurity() method creates a Stream object, FS, that it uses to open Temp.TXT for read access. Given that this action is denied, the open should fail. In this case, you do see the error message shown in Figure 3.
At this point, btnTest_Click() calls OverrideSecurity(). However, this time the code begins by creating a FileIOPermission object, FIOP. This object is designed to request the FileIOPermissionAccess.Read permission for Temp.TXT.
It demonstrates the imperative approach for setting permissions (as
contrasted to the declarative approach used earlier in this section).
When the code calls FIOP.Assert(), the system grants access to the file. The code then uses precisely the same technique as in the UseSecurity() method to create a Stream object, FS.
In this case, the call is successful, so you see the success message.
After the user clicks OK, the code simply closes the file by calling FS.Close().
The point of this exercise is that permissions set using the classes in the System.Security.Permissions
namespace are fluid. Code can easily change them as needed, which is
why Microsoft is apparently abandoning at least the policy part of CAS.
Still, this example shows how to implement the old-style code should
you need it.
NOTE
This approach does come with some
limitations, so you need to use it carefully. Native code images
produced with the NGen utility can run slower. The basic reason for the
performance loss is that the code marked with the <NetFx40_LegacySecurityPolicy>
element must run as a Just-in-Time (JIT, pronounced "jit") assembly,
rather than a native code image. In short, the CLR must compile the
code every time before it can run it.