[ Team LiB ] |
10.2 Programming Role-Based SecurityThe .NET runtime enforces role-based security using techniques and syntax similar to those we described in Chapter 7 for code-access security. In your applications, you protect functionality by making role-based security demands that specify the identity or role that the thread's principal must contain. If the thread's principal does not contain the demanded identity and role, then the demand causes an exception. 10.2.1 Introducing the IIdentity and IPrincipal InterfacesThe System.Security.Principal namespace includes the IIdentity and IPrincipal interfaces to represent identities and principals. By using interfaces to represent identities and principals, .NET provides flexibility, which means that it is relatively easy to create concrete role-based security implementations to support many different authentication and authorization mechanisms. The .NET class library contains four concrete RBS implementations that use the IIdentity and IPrincipal interfaces:
We describe the members of the IIdentity and IPrincipal interfaces in Table 10-1. In later sections, when we discuss the Windows and Generic implementations of these interfaces, we highlight any implementation-specific behavior.
10.2.2 Determining the Current PrincipalEvery thread executing in the .NET runtime has an IPrincipal object associated with it. The thread's IPrincipal object represents the user on whose behalf the thread is running, and allows the runtime to make role-based security decisions for the thread based on the user's identity and roles. However, many applications do not use the RBS features of .NET, and so in the interest of performance and conserving resources, the .NET runtime does not automatically assign an IPrincipal object to every thread. If you intend to use RBS, you must either assign an IPrincipal to a thread manually or configure the runtime to create one automatically the first time it is needed. You can use the System.AppDomain.SetThreadPrincipal method shown in the following code to specify an IPrincipal object that the runtime will automatically assign to each thread running in the application domain. You can call the SetThreadPrincipal method only once on each application domain; otherwise, an instance of System.Security.Policy.PolicyException is thrown: # C# public void SetThreadPrincipal( IPrincipal principal ); # Visual Basic .NET NotOverridable Public Sub SetThreadPrincipal( _ ByVal principal As IPrincipal _ ) Instead of using SetThreadPrincipal to set a default IPrincipal for an entire application domain, you can set the current thread's IPrincipal manually through the System.Threading.Thread.CurrentPrincipal property. CurrentPrincipal is static (C#) or Shared (Visual Basic .NET) and always affects the IPrincipal of the currently executing thread. You also use CurrentPrincipal to obtain the IPrincipal of the current thread.
If you do not use AppDomain.SetThreadPrincipal or Thread.CurrentPrincipal to assign an IPrincipal to a Thread, the principal policy of the application domain in which the thread is running determines what happens when code tries to obtain the thread's CurrentPrincipal. You can configure the principal policy of an application domain using the System.AppDomain.SetPrincipalPolicy method, which has the following signature: # C# public void SetPrincipalPolicy( PrincipalPolicy policy ); # Visual Basic .NET NotOverridable Public Sub SetPrincipalPolicy( _ ByVal policy As PrincipalPolicy _ ) The policy argument is a member of the System.Security.Principal.PrincipalPolicy enumeration, whose values we list in Table 10-2. The default principal policy for all application domains is UnauthenticatedPrincipal, which creates an empty IPrincipal that is not useful for making role-based security decisions.
As you can see from the three principal policy options, if you want to use an RBS implementation in your programs that does not rely the current Windows user, principal policy provides no benefit. It is your responsibility to ensure that the current thread has the correct IPrincipal associated with it. You must create the IPrincipal object and assign it to the thread using the AppDomain.SetThreadPrincipal method or the Thread.CurrentPrincipal property. 10.2.3 Programming the Windows Role-Based Security ImplementationThe WindowsIdentity and WindowsPrincipal classes of the System.Security.Principal namespace provide an RBS implementation that allows you to base security decisions on the identity and roles of Windows user accounts. The WindowsIdentity class implements the IIdentity interface and represents a Windows user account. The Windows user's name, accessible through the WindowsIdentity.Name property, is of the form "Domain\User." The WindowsPrincipal class implements IPrincipal and contains the user's WindowsIdentity object, along with the names of the Windows groups to which the user belongs. The name of any built-in Windows user groups has the prefix "BUILTIN\"—for example, "BUILTIN\Administrators" or "BUILTIN\Users". This is important to remember when using the WindowsPrincipal.IsInRole method to test role membership. Non-built-in group names are prefixed with the name of the domain to which the group belongs, or the machine name if the group exists on a standalone machine—for example, "MyDomain\Developers" or "MyMachine\Developers".
The WindowsIdentity class implements members in addition to those defined in the IIdentity interface. These additional members provide useful functionality for creating WindowsIdentity objects, testing what type of Windows account a WindowsIdentity object represents, and impersonating Windows users. We summarize the members of the WindowsIdentity class in Table 10-3.
The WindowsPrincipal class implements no role-based functionality other than that specified by the IPrincipal interface. The Identity property returns the contained WindowsIdentity as an IIdentity object; therefore, you must cast it to WindowsIdentity before you can use the additional methods listed in Table 10-3. Of note is the implementation of the IsInRole method, which has three overloads: # C# public virtual bool IsInRole( int rid ); public virtual bool IsInRole( string role ); public virtual bool IsInRole( WindowsBuiltInRole role ); # Visual Basic .NET Overridable Overloads Public Function IsInRole( _ ByVal rid As Integer _ ) As Boolean Overridable Overloads Public Function IsInRole( _ ByVal role As String _ ) As Boolean Overridable Overloads Public Function IsInRole( _ ByVal role As WindowsBuiltInRole _ ) As Boolean In the first overload, the rid argument specifies a Windows Role Identifier (RID). RIDs are a component of a Windows group's security identifier and provide a way to identify groups independent of language localization. The second overload takes a case-insensitive string that specifies the name for which to test; this is the standard form of IsInRole defined in IPrincipal. The final overload takes a member of the System.Security.Principal.WindowsBuiltInRole enumeration as an argument; WindowsBuiltInRole contains values that represent the standard Windows groups. Table 10-4 compares the role names, RIDs, and WindowsBuiltInRole values used to test for membership of the most commonly used Windows groups.
10.2.3.1 Configuring the current WindowsPrincipalThe easiest way to make the current principal represent the active Windows user is by calling the System.AppDomain.SetPrincipalPolicy method and passing it the value PrincipalPolicy.WindowsPrincipal. The first time code requests the thread's IPrincipal, the runtime creates a WindowsIdentity and a WindowsPrincipal for a thread based on the currently active Windows access token: # C# // Configure the current application domain's principal policy // to represent the active Windows user AppDomain.CurrentDomain.SetPrincipalPolicy( PrincipalPolicy.WindowsPrincipal); # Visual Basic .NET ' Configure the current application domain's principal policy ' to represent the active Windows user AppDomain.CurrentDomain.SetPrincipalPolicy( _ PrincipalPolicy.WindowsPrincipal) You can also create WindowsIdentity and WindowsPrincipal objects manually and pass them to the AppDomain.SetThreadPrincipal method or the Thread.CurrentPrincipal property. The AppDomain.SetThreadPrincipal defines the default principal that the runtime assigns to any thread in the application domain, whereas the Thread.CurrentPrincipal property sets the principal of the current thread. The WindowsIdentity.GetCurrent method returns a WindowsIdentity object that represents the current Windows user. Once you have a WindowsIdentity object, you can pass it to the WindowsPrincipal constructor and assign the new WindowsPrincipal to the current thread, as demonstrated by the following statements: # C# // Create a WindowsIdentity for the active Windows user WindowsIdentity wi = WindowsIdentity.GetCurrent( ); // Create a new WindowsPrincipal WindowsPrincipal wp = new WindowsPrincipal(wi); // Assign the WindowsPrincipal to the active thread. Thread.CurrentPrincipal = wp; # Visual Basic .NET ' Create a WindowsIdentity for the active Windows user Dim wi As WindowsIdentity = WindowsIdentity.GetCurrent( ) ' Create a new WindowsPrincipal Dim wp As WindowsPrincipal = New WindowsPrincipal(wi) ' Assign the WindowsPrincipal to the active thread. Thread.CurrentPrincipal = wp Using the Thread.CurrentPrincipal property or the AppDomain.SetThreadPrincipal method allows you to change the thread's current principal if you need your code to operate on behalf of a different principal—a process known as impersonation. However, changing the current principal does not change the current Windows access token. 10.2.3.2 Impersonating Windows usersSometimes, you will want your code to act at the operating system level as though it is a different user than the one currently logged on. This is particularly common in server applications that process requests from different users and need to access resources, such as databases, on behalf of the user. The WindowsIdentity class provides the mechanism through which you can impersonate another Windows user. However, first you must obtain a Windows access token that represents the user you want to impersonate. Unfortunately, the .NET Framework class library does not currently contain classes that provide managed access to the Windows account database, and therefore you must call the LogonUser method of the unmanaged advapi32.dll Win32 library to obtain the access token. The LogonUser method takes username and password arguments, along with other parameters that control the authentication process, and provides access to an access token for the user. If the LogonUser fails, you will also need to call the System.Runtime.InteropServices.Marshal.GetLastWin32Error method to determine what the problem is. The GetLastWin32Error method exposes the GetLastError method of the kernel32.dll library and avoids possible problems arising from the CLR making internal calls to the WIN32 APIs that would overwrite the last error code.
Once you have the access token, you use it to create a new WindowsIdentity object and call its Impersonate method. The Impersonate method changes the Windows access token of the current thread. Any operating system operation you perform after calling Impersonate takes place as the impersonated user. The Impersonate method returns a System.Security.Principal.WindowsImpersonationContext object that represents the Windows user prior to impersonation. When you want to revert to the original user, call the WindowsImpersonationContext.Undo method. Example 10-1 demonstrates the impersonation of a Windows user named "Bob." Although the example demonstrates how to call LogonUser from managed code, a complete discussion of unmanaged code interoperability and the LogonUser method is beyond the scope of this book. You should consult the .NET Framework SDK documentation for details on calling unmanaged code and the Windows Platform SDK for details of the LogonUser method. Example 10-1. Impersonating a Windows user# C# using System; using System.IO; using System.Security.Principal; using System.Security.Permissions; using System.Runtime.InteropServices; // Make sure we have permission to execute unmanged code [assembly:SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode=true)] public class WindowsImpersonationTest { // Define the external LogonUser method from advapi32.dll. [DllImport("advapi32.dll", SetLastError=true)] static extern int LogonUser(String UserName, String Domain, String Password, int LogonType, int LogonProvider, ref IntPtr Token); public static void Main( ) { // Create a new initialized IntPtr to hold the access token // of the user to impersonate. IntPtr token = IntPtr.Zero; // Call LogonUser to obtain an access token for the user // "Bob" with the password "treasure". We authenticate against // the local accounts database by specifying a "." as the Domain // argument. int ret = LogonUser(@"Bob", ".", "treasure", 2, 0, ref token); // If the LogonUser return code is zero an error has occured. // Display it and exit. if (ret == 0) { Console.WriteLine("Error {0} occured in LogonUser", Marshal.GetLastWin32Error( )); return; } // Create a new WindowsIdentity from Bob's access token WindowsIdentity wi = new WindowsIdentity(token); // Impersonate Bob, saving a reference to the returned // WindowsImpersonationContext. WindowsImpersonationContext impctx = wi.Impersonate( ); // !!! Perform actions as Bob !!! // We create a file that Windows will show is owned by Bob. StreamWriter file = new StreamWriter("test.txt"); file.WriteLine("Bob's test file."); file.Close( ); // Revert back to the original Windows user using the // WindowsImpersonationContext object. impctx.Undo( ); } } # Visual Basic .NET Imports System Imports System.IO Imports System.Security.Principal Imports System.Security.Permissions Imports System.Runtime.InteropServices ' Make sure we have permission to execute unmanged code <assembly:SecurityPermission(SecurityAction.RequestMinimum, _ UnmanagedCode:=True)> _ Public Class WindowsImpersonationTest ' Define the external LogonUser method from advapi32.dll. <DllImport("advapi32.dll", SetLastError := True)> _ Public Shared Function LogonUser(UserName As String, _ Domain As String, Password As String, LogonType As Integer, _ LogonProvider As Integer, ByRef Token As IntPtr) As Integer End Function Public Shared Sub Main( ) ' Create a new initialized IntPtr to hold the access token ' of the user to impersonate. Dim token As IntPtr = IntPtr.Zero ' Call LogonUser to obtain an access token for the user ' "Bob" with the password "treasure". We authenticate against ' the local accounts database by specifying a "." as the Domain ' argument. Dim ret As Integer = LogonUser("Bob",".","treasure",2,0, token) ' If the LogonUser return code is zero an error has occured. ' Display it and exit. If ret = 0 Then Console.WriteLine("Error {0} occured in LogonUser", _ Marshal.GetLastWin32Error( )) Return End If ' Create a new WindowsIdentity from Bob's access token Dim wi As WindowsIdentity = New WindowsIdentity(token) ' Impersonate Bob, saving a reference to the returned ' WindowsImpersonationContext. Dim impctx As WindowsImpersonationContext = wi.Impersonate( ) ' !!! Perform actions as Bob !!! ' We create a file that Windows will show is owned by Bob. Dim file As StreamWriter = New StreamWriter("test.txt") file.WriteLine("Bob's test file.") file.Close( ) ' Revert back to the original Windows user using the ' WindowsImpersonationContext object. impctx.Undo( ) End Sub End Class 10.2.4 Making Role-Based Security DemandsRole-based security demands function similarly to code-access security demands. The key difference is that role-based security demands never result in a stack walk. The result of a role-based security demand is based solely on the identity and roles of the active thread's principal. If the thread's principal does not contain the demanded identity or roles, the runtime throws a System.Security.SecurityException. If the principal meets the demanded requirements, execution continues unaffected. The PrincipalPermission class, with its attribute counterpart named PrincipalPermissionAttribute, provides the mechanisms through which you invoke role-based security demands in your programs. The PrincipalPermission class allows you to use imperative security syntax, and the PrincipalPermissionAttribute class enables the use of declarative syntax; both classes are members of the System.Security.Permissions namespace. We discussed the syntax and use of both imperative and declarative security demands in Chapter 7, and therefore we will only cover them briefly in the following sections.
10.2.4.1 Using imperative role-based security statementsTo make an imperative RBS demand, you must instantiate a PrincipalPermission object and call its Demand method. The most commonly used PrincipalPermission constructor has the signature shown here: # C# public PrincipalPermission( string name, string role ); # Visual Basic .NET Public Sub New( _ ByVal name As String, _ ByVal role As String _ ) The name and role arguments specify the values that the current principal must have for the Demand to succeed. Each PrincipalPermission can specify only a single role name. The current principal must match both the specified name and role values; however, you can specify null (C#) or Nothing (Visual Basic .NET) for either argument to match any value. Table 10-5 explains how PrincipalPermission uses these arguments to in determine the result of the Demand.
The following statements demonstrate the use of the PrincipalPermission class to invoke a variety of imperative RBS demands: # C# // Demand that the active principal has the identity name "Peter". PrincipalPermission p1 = new PrincipalPermission("Peter", null); p1.Demand( ); // Demand that the active principal is a member of the "Developers" // group with any identity. PrincipalPermission p2 = new PrincipalPermission(null,"Developers"); p2.Demand( ); // Demand that the active principal have the identity name "Bart" // and be a member of the "Managers" group. PrincipalPermission p3 = new PrincipalPermission("Bart", "Managers"); p3.Demand( ); # Visual Basic .NET ' Demand that the active principal has the identity name "Peter". Dim p1 As PrincipalPermission = _ New PrincipalPermission("Peter",Nothing) p1.Demand( ) ' Demand that the active principal is a member of the "Developers" ' group with any identity. Dim p2 As PrincipalPermission = _ New PrincipalPermission(Nothing,"Developers") p2.Demand( ) ' Demand that the active principal have the identity name "Bart" ' and be a member of the "Managers" group. Dim p3 As PrincipalPermission = _ New PrincipalPermission("Bart","Managers") p3.Demand( ) PrincipalPermission implements the System.Security.IPermission interface, and defines the Copy, Intersect, IsSubsetOf, and Union methods to manipulate PrincipalPermission objects. The composition of PrincipalPermission objects means the Intersect and IsSubSetOf methods are of little value; PrincipalPermission primarily implements them to satisfy the requirements of the IPermission interface. The Union method, however, does provide useful functionality, enabling you to make security demands that test against multiple sets of name/role pairs. The Union of two PrincipalPermission objects is a PrincipalPermission object that contains the name and role elements of both source objects. For example if the two source PrincipalPermission objects contained the name/role values "Alice"/"managers" and "Bob"/"developers," the union would contain both name/role pairs. Calling Demand on the resulting PrincipalPermission would succeed if the current principal matches either name/role pair. However, PrincipalPermission maintains and compares the contained name/role pairs independently. Comparing the union against a principal with the name/role pair "Alice"/"developers" would fail. To understand the contents of a PrincipalPermission resulting from a Union, look at the following output from the PrincipalPermission.ToString method: <Permission class="System.Security.Permissions.PrincipalPermission, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1"> <Identity Authenticated="true" ID="Alice" Role="managers"/> <Identity Authenticated="true" ID="Bob" Role="developers"/> </Permission> 10.2.4.2 Using declarative role-based security statementsYou can apply PrincipalPermissionAttribute to classes, methods, properties, or events to force declarative security demands. The key difference between PrincipalPermissionAttribute and the permission attributes (see Chapter 7) is that you cannot apply the PrincipalPermisisonAttribute at the assembly level. This means you cannot make role-based permission requests, such as RequestMinimum, RequestOptional, and RequestRefuse. In addition, because RBS does not use or affect the call stack, there are no declarative stack override statements available. Thus, you can only invoke the following three role-based security statements using declarative syntax: Demand, LinkDemand, and InheritenceDemand. The PrincipalPermissionAttribute class defines the Name and Role properties that you use to specify the name and role values that the current principal must have for the declarative RBS demand to succeed. The following statements demonstrate the equivalent PrincipalPermissionAttribute syntax used to make the same demands we made in the previous section: # C# // Demand that the active principal has the identity name "Peter". [PrincipalPermission(SecurityAction.Demand, Name = "Peter")] // Demand that the active principal is a member of the "Developers" // group with any identity. [PrincipalPermission(SecurityAction.Demand, Role = "Developers")] // Demand that the active principal have the identity name "Bart" // and be a member of the "Managers" group. [PrincipalPermission(SecurityAction.Demand, Name = "Bart", Role = "Managers")] # Visual Basic .NET ' Demand that the active principal has the identity name "Peter". <PrincipalPermission(SecurityAction.Demand, Name := "Peter")> _ ' Demand that the active principal is a member of the "Developers" ' group with any identity. <PrincipalPermission(SecurityAction.Demand, Role := "Developers")> _ ' Demand that the active principal have the identity name "Bart" ' and be a member of the "Managers" group. <PrincipalPermission(SecurityAction.Demand, Name:="Bart", Role:="Managers")> _ 10.2.5 Programming the Generic Role-Based Security ImplementationIf you rely on an authority other than the Windows accounts system to authenticate and authorize your users, it is possible that the manufacturers of the system will provide IIdentity and IPrincipal implementations that work directly with the authority. If not, the simplest approach is to obtain identity and role information from the custom authority and use the GenericIdentity and GenericPrincipal classes from the System.Security.Principal namespace to enable role-based security support in your managed applications. The GenericIdentity and GenericPrincipal classes provide a generic RBS implementation that you can use independently of the Windows user accounts database. The GenericIdentity class implements IIdentity, and GenericPrincipal implements IPrincipal. 10.2.5.1 Configuring the current GenericPrincipalThe techniques for setting the current principal to a GenericPrincipal object are more limited than those we described earlier in Section 10.2.3.1. It is impossible for an application domain's principal policy to create useful GenericPrincipal objects automatically; therefore, you have two options:
To create a GenericPrincipal, you must first create a GenericIdentity. The GenericIdentity class provides two constructors with the following signatures: # C# public GenericIdentity( string name ); public GenericIdentity( string name, string type ); # Visual Basic .NET Public Sub New( _ ByVal name As String _ ) Public Sub New( _ ByVal name As String, _ ByVal type As String _ ) The first constructor takes a single name argument, in which you specify the name of the user represented by the identity. In addition to the name argument, the second constructor takes a type argument. The type argument allows you to specify a string that identifies the type of authentication mechanism used to authenticate the user. The GenericIdentity class enforces no restrictions on the format or content of the name and type arguments as long as they contain only valid string characters. This provides maximum flexibility, which allows you to use the GenericIdentity class in conjunction with any custom authentication mechanisms. The following statements demonstrate the use of the GenericIdentity constructors: # C# GenericIdentity i1 = new GenericIdentity("Peter"); GenericIdentity i2 = new GenericIdentity("Peter","SmartCard"); # Visual Basic .NET Dim i1 As GenericIdentity = New GenericIdentity("Peter") Dim i2 As GenericIdentity = New GenericIdentity("Peter","SmartCard") The GenericPrincipal class provides a single constructor that takes an IIdentity object and a string array as arguments: # C# public GenericPrincipal( IIdentity identity, string[] roles ); # Visual Basic .NET Public Sub New( _ ByVal identity As IIdentity, _ ByVal roles( ) As String _ ) The identity argument can be of any type that implements IIdentity and represents the user that the GenericPrincipal should represent. The roles argument takes a string array containing the set of role names to which the user belongs. You must specify the user roles in the GenericPrincipal object constructor because the GenericPrincipal class is independent of any underlying user-authorization mechanism. The following statements demonstrate the creation of a GenericPrincipal from a GenericIdentity, and then make the new GenericPrincipal the current principal using the Thread.CurrentPrincipal property: # C# // Create a GenericIdentity for the user Peter GenericIdentity gi = new GenericIdentity("Peter"); // Create a GenericPrincipal for Peter and specify membership of the // Developers and Managers roles String[] roles = new String[]{"Developers", "Managers"}; GenericPrincipal gp = new GenericPrincipal(gi, roles); // Assign the new principal to the current thread Thread.CurrentPrincipal = gp; # Visual Basic .NET ' Create a GenericIdentity for the user Peter Dim gi As GenericIdentity = New GenericIdentity("Peter") ' Create a GenericPrincipal for Peter and specify membership of the ' Developers and Managers roles Dim roles( ) As String = New String( ) {"Developers", "Managers"} Dim gp As GenericPrincipal = New GenericPrincipal(gi,roles) ' Assign the new principal to the current thread Thread.CurrentPrincipal = gp; Both the GenericIdentity and GenericPrincipal classes are simple, offering no role-based functionality other than that defined in their respective IIdentity and IPrincipal interfaces. In Table 10-6, we describe the GenericIdentity and GenericPrincipal classes' implementations of the IIdentity and IPrincipal interfaces.
|
[ Team LiB ] |