Creating a Microsoft Installer File to Manage Security Policies
Downloading and running a .NET assembly from the Internet is not an automatic given. Security settings (see Chapter 18) are likely to change from customer to customer, and the default settings will evolve over time as more is understood about customers' basic security needs. If you are providing executable assemblies for download from the Internet, you may need to provide a reliable means for your customers to be able to run those assemblies. You could count on the customers knowing how to manage the security settings for .NET themselves ; but if some actually don't know them, good customers may walk away from your application as a viable solution. As an alternative you could provide written instructions that describe how to manage security settings, or (the best alternative) you could automate the security modifications on the customers' behalf .
In this section we take a look at how to manually and programmatically manage permissions for smart client applications. There is enough information in this section for you to successfully perform the tasks described, but you will need to read Chapter 18 for more general background information on .NET security.
Defining a New Code Group
There are several ways to manage security. You can use the caspol .exe utility to manage code access security policy from the command line or a batch file, or you can use the .NET Framework Configuration utility console snap-in to manage security with a graphical user interface. We will use the .NET Framework Configuration snap-in because it is more convenient .
Suppose that a customer clicks on a link to your executable assembly and instead of the application starting up, the user sees an error message or, worse yet, nothing happens. Most usersincluding mehave a very short fuse when it comes to waiting for content from the Web. If the application doesn't start up and there is no apparent easy remedy, the user is likely to go to the next cool thing on the Web.
As techies we can manage security permissions on our machines, and we should be able to explain them to someone else if the need arises or we need to programmatically modify permissions for our applications. Back to our scenario.
For demonstration purposes, let's say you had problems running the Football game we talked about earlier in the chapter. You click on Football.exe and it doesn't run. You can give it permission to run by following the steps below.
When you are done, you should see the Softconcepts Football child group in the All_Code code group.
Manually setting permissions isn't too difficult to do on an occasional basis; however, many Internet users may not be comfortable setting security permissions or may themselves not have sufficient privileges on their machines to do so. In addition, if you had to manage permissions for a wide variety of applications, it would become tedious to do so manually. We have the option of writing code to manage permissions for us; this approachonce the code has been perfectedis less prone to error and is much faster.
Managing Code Groups and Permissions Programmatically
Support for managing code groups and permissions can be found in the System.Security namespace. The classes in this namespace support performing operations analogous to those we performed in the .NET Framework Configuration tool. All we have to do is convert the operations to method signatures. To summarize, we need to complete the following tasks.
Listing 10.3 contains an example console application that adds a custom code group to the machine policy. The code group is approximately identical to the policy changes we made manually in the preceding section. You can find this console application, SecuritySetupTest.vbproj , as part of Football.sln .
Listing 10.3 Managing Security Policy Programmatically in .NET
1: Imports System.Security.Policy 2: Imports System.Security.Permissions 3: Imports System.Security ' SecurityManager 4: Imports System.ComponentModel ' RunInstallerAttribute 5: Imports System.Collections ' IEnumerator 6: 7: Public Module Module1 8: 9: Public Sub Main() 10: Dim machinePolicy As PolicyLevel = GetMachinePolicyLevel() 11: If (Not (machinePolicy Is Nothing)) Then 12: 13: Dim permissions As PermissionSet = _ 14: New NamedPermissionSet("Internet") 15: 16: Dim policy As PolicyStatement = _ 17: New PolicyStatement(permissions) 18: 19: Dim membership As IMembershipCondition = _ 20: New UrlMembershipCondition( _ 21: "http://www.softconcepts.com/Football/Football.exe") 22: 23: Dim group As CodeGroup = _ 24: New UnionCodeGroup(membership, policy) 25: 26: group.Description = "Permissions for Softconcepts Football" 27: group.Name = "Software Conceptions, Inc." 28: machinePolicy.RootCodeGroup.AddChild(group) 29: SecurityManager.SavePolicy() 30: 31: Else 32: Debug.WriteLine("Failed to find user policy.") 33: End If 34: 35: End Sub 36: 37: 38: Private Function GetMachinePolicyLevel() As PolicyLevel 39: Dim policies As IEnumerator = _ 40: SecurityManager.PolicyHierarchy 41: 42: While (policies.MoveNext()) 43: 44: If (CType(policies.Current, _ 45: PolicyLevel).Label = "Machine") Then 46: 47: Return CType(policies.Current, PolicyLevel) 48: End If 49: End While 50: 51: Return Nothing 52: 53: End Function 54: 55: End Module
Lines 38 through 53 use an enumerator to find the machine policy. There should be an indexer and enumerator on PolicyHierarchy to simplify access. There may not be one to more readily support future policy levels.
The Main subroutine in lines 9 through 35 actually performs the steps to add the code group to the machine policy. Line 10 searches PolicyHierarchy to obtain the correct PolicyLevel object. If the machinePolicy object is found (line 11), we create an instance of the Internet permission set in lines 13 and 14. The permission set is used to create an instance of a PolicyStatement object (lines 16 and 17). Lines 19 through 21 set a membership condition. (The assembly must come from http://www.softconcepts.com/Football/Football.exe.)
Finally, the new code group is created, adding the membership condition and policy to the group, and a name and description are provided. The last step is to save the policy (line 29). When the code has finished running, you should be able to search the security.config file and find the entry for Software Conceptions, Inc.
Writing code to programmatically change policy settings is reliable once the code works correctly. However, this approach won't work for customers who can't run the assembly from the Internet because the policy modification application won't run for the same reason the original assembly didn't run. We have a solution to this problem too.
Managing Permissions by Using an Installer
Microsoft Installer ( .msi ) files are permitted to run from the Internet because they require user interactionthe user acknowledges operations as the installer runs. For this reason we can add security permissions to a .msi file, and a customer can run this .msi file to update the security policy. The actual code for modifying the security policy is almost the same, but we do need to create the .msi project and write the policy modification code as part of an Installer class. I will walk you through the steps of defining the setup project and show the complete listing for the installer. (You can download the complete solution as part of Football.sln , which includes the installer and the setup project.)
Implementing an Installer Library
The System.Configuration.Install.Installer class is the base class for custom installers . We can define a class in a class library project and add the project to a .msi project. The Microsoft Windows Installer will load and run the code in the custom installer. For this mechanism to work we have to inherit from System.Configuration.Install.Installer and apply the RunInstallerAttribute to the custom Installer class. Armed with this information, the Windows Installer will run our custom installer. We can program the custom installer to perform any installation task we might need, including modifying the security policy. Because .msi files are permitted to run from the Internet, this is an ideal way to update a customer's security policy.
Listing 10.4 contains the source code for our custom installer. Note the similarities between the installer and the console application discussed in the preceding subsection (Listing 10.3).
Listing 10.4 Implementing a Custom Installer
1: Imports System.Security.Policy 2: Imports System.Configuration.Install 3: Imports System.Security.Permissions 4: Imports System.Security ' SecurityManager 5: Imports System.ComponentModel ' RunInstallerAttribute 6: Imports System.Collections ' IEnumerator 7: 8: <RunInstaller(True)> _ 9: Public Class Installer1 10: Inherits System.Configuration.Install.Installer 11: 12: Public Sub New() 13: 14: Dim machinePolicy As PolicyLevel = GetMachinePolicyLevel() 15: If (Not (machinePolicy Is Nothing)) Then 16: 17: Dim permissions As PermissionSet = _ 18: New NamedPermissionSet("Internet") 19: 20: Dim policy As PolicyStatement = _ 21: New PolicyStatement(permissions) 22: 23: Dim membership As IMembershipCondition = _ 24: New UrlMembershipCondition( _ 25: "http://www.softconcepts.com/Football/Football.exe") 26: 27: Dim group As CodeGroup = _ 28: New UnionCodeGroup(membership, policy) 29: 30: group.Description = "Permissions for Softconcepts Football" 31: group.Name = "Software Conceptions, Inc." 32: machinePolicy.RootCodeGroup.AddChild(group) 33: SecurityManager.SavePolicy() 34: 35: Else 36: Debug.WriteLine("Failed to find the machine policy.") 37: End If 38: 39: End Sub 40: 41: 42: Private Function GetMachinePolicyLevel() As PolicyLevel 43: Dim policies As IEnumerator = _ 44: SecurityManager.PolicyHierarchy 45: 46: While (policies.MoveNext()) 47: 48: If (CType(policies.Current, _ 49: PolicyLevel).Label = "Machine") Then 50: 51: Return CType(policies.Current, PolicyLevel) 52: End If 53: End While 54: 55: Return Nothing 56: 57: End Function 58: 59: End Class
There is little need to repeat the elaboration of the code in Listing 10.4; it is almost identical to the code in Listing 10.3. The notable differences are that in Listing 10.4 we define public class Installer1 as inheriting from System.Configuration.Install.Installer (line 10), indicating that Installer1 is a custom installer. The RunInstallerAttribute indicates that an installer application should be run during the installation of the assembly containing this custom installer.
Now all we have to do is add SecuritySetup.vbproj , which contains the custom installer, to a setup project and tell the setup project what to do with the library.
Defining the Setup Project
Setup Project templates ship with Visual Studio .NET. (Check your version of Visual Studio .NET to determine whether the Setup Project template ships with your specific version.) We can add a Windows Installer setup project to Football.sln , which also contains SecuritySetup.vbproj , and use this project to manage the security policy modifications on our behalf.
Defining the setup project is a straightforward process. Follow these steps.
At this point we are all set. We can build our entire solution, which includes the SecuritySetup.dll assembly and the SetupFootballPermissions setup project. After we have built the project we can test the installation right from Visual Studio .NET. VS .NET will roll back previous installations and run the Windows Installer. If the custom installer makes the correct modifications to the security policy, we are ready to deploy the .msi file. Placing this file conveniently on your Web site will give your customers an easy remedy in the event they cannot run your executable assembly from the Web.