Section 12.5. Principal-Based Security


12.5. Principal-Based Security

As stated at the beginning of this chapter, .NET component-based security isn't a cure-all. There is still a need to verify that the user (or the account) under which the code executes has permission to perform the operation. In .NET, the user is referred to as the security principal. It's impractical to program access permissions for each individual user (although it's technically possible); instead, it is better to grant permissions to roles users play in the application domain. A role is a symbolic category of users who share the same security privileges. When you assign a role to an application resource, you are granting access to that resource to whomever is a member of that role. Discovering the roles users play in your business domain is part of your application-requirement analysis and design, as is factoring components and interfaces. By interacting with roles instead of particular users, you isolate your application from changes made in real life, such as adding new users, moving existing users between positions, promoting users, or users leaving their jobs. .NET allows you to apply role-based security both declaratively and programmatically, if the need to verify role membership is based on a dynamic decision.

12.5.1. Declarative Role-Based Security

Apply declarative role-based security using the attribute PrincipalPermissionAttribute, defined in the System.Security.Permissions namespace:

     [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method                     AllowMultiple = true,Inherited = false)]     public sealed class PrincipalPermissionAttribute : CodeAccessSecurityAttribute     {        public PrincipalPermissionAttribute(SecurityAction action);        public bool Authenticated{get;set;}        public string Name{get;set;}        public string Role{get;set;}     } 

You apply the attribute to either classes or methods, specifying the security action to take and the role name. By default, a security role in .NET is a Windows user group. The examples in this chapter all use Windows user groups, but .NET allows you to provide your own custom role definitions, as demonstrated in Appendix B. When you specify a Windows user group as a role, you must prefix it with the domain name or the local machine name (if the role is defined locally only). For example, the following declaration grants access to MyMethod( ) only for code running under the identity of a user belonging to the Managers user group:

     public class MyClass     {        [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Managers")]        public void MyMethod(  )        {...}     } 

If the user isn't a member of that role, .NET throws an exception of type SecurityException. If multiple roles are allowed to access the method, you can apply the attribute multiple times:

     [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Managers")]     [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Customers")]     public void MyMethod(  )     {...} 

When multiple roles are applied to a method, the user is granted access if the user is a member of at least one role. If you want to verify that the user is a member of both roles, you need to use programmatic role-membership checks, discussed later.

When it comes to PrincipalPermissionAttribute, SecurityAction.Demand behaves like SecurityAction.DemandChoice. You cannot combine SecurityAction.DemandChoice with PrincipalPermissionAttribute. This inconsistency originated with .NET 1.1, which did not have the SecurityAction.DemandChoice value.


You can apply the PrincipalPermissionAttribute at the class level as well:

     [PrincipalPermission(SecurityAction.Demand,Role=@"<domain>\Managers")]     public class MyClass     {        public void MyMethod(  )        {...}     } 

When the attribute is applied at the class level, only clients belonging to the specified role can create an object of this type. This is the only way to enforce role-based security on constructors, because you can't apply PrincipalPermissionAttribute on class constructors.

By setting the Name property of PrincipalPermissionAttribute, you can even insist on granting access to a particular user:

     [PrincipalPermission(SecurityAction.Demand,Name = "Bill")] 

You can also insist on a particular user and insist that the user be a member of a specific role:

     [PrincipalPermission(SecurityAction.Demand,Name="Bill",Role=@"<domain>\Managers")] 

This practice is inadvisable, however, because hardcoding usernames is fragile.

12.5.1.1 Enabling role-based security

Every app domain has a flag that instructs .NET which principal policy to use. The principal policy is the authorization mechanism that looks up role membership. You set the principal policy by calling the SetPrincipalPolicy( ) method of the AppDomain class:

     public void SetPrincipalPolicy(PrincipalPolicy policy); 

The available policies are represented by the values of the PrincipalPolicy enum:

     public enum PrincipalPolicy     {         NoPrincipal,         UnauthenticatedPrincipal,         WindowsPrincipal     } 

By default, every .NET application (be it Windows Forms or ASP.NET) has PrincipalPolicy.UnauthenticatedPrincipal specified for its security policy. If you simply apply PrincipalPermissionAttribute (or use programmatic role-membership verification), all calls will be denied access, even if the caller is a member of the specified role.

To use role-based security in ASP.NET, the caller must be authenticated. With authenticated callers in ASP.NET, there is no need to call SetPrincipalPolicy( ), although it doesn't cause harm.

To enable role-based security in a Windows application, you must set the role-based security policy to PrincipalPolicy.WindowsPrincipal. It is a good idea to use this value even if you install a custom role-based security mechanism, as in Appendix B. This is because PrincipalPolicy.WindowsPrincipal enables the use of Windows accounts, and you have no idea if other components will try to do that. You need to set the principal policy in every app domain that uses role-based security. Typically, you place that code in the Main( ) method of an application assembly:

     static public void Main(  )     {        AppDomain currentDomain = AppDomain.CurrentDomain;        currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);     } 

If you create new app domains programmatically, you also need to set the principal policies in them.

When you experiment with Windows role-based security, you often add users to or remove users from user groups. Because Windows caches user-group information at login time, the changes you make aren't reflected until the next login.


12.5.1.2 Role-based security and authentication

Role-based security controls user authorization that is, what users are allowed to access. However, authorization is meaningless without authentication, or verification that the user is indeed who the user claims to be. In a Windows application, users have to log in and are therefore authenticated. Internet applications (such as ASP.NET applications or web services), on the other hand, sometimes grant anonymous access to users. It's therefore prudent to verify that users are authenticated when applying role-based authorization, in case your components are used in a non-authenticating environment. You can demand authentication by setting the Authenticated property of the PrincipalPermissionAttribute to true:

     [PrincipalPermission(SecurityAction.Demand, Authenticated = true,                                                             Role=@"<domain>\Managers")] 

Declarative role-based security hardcodes the role name. If your application is deployed in international markets and you use Windows groups as roles, it's likely that the role names will not match. In that case, you have to use programmatic role verification and have some logic that maps the logical design-time roles to the local roles.


12.5.2. Programmatic Role-Based Security

As handy as declarative role-based security is, sometimes you need to programmatically verify role membership. Usually, you need to do that when the decision as to whether to grant access depends both on role membership and on some other values known only during call time. Another case in which programmatic role-membership verification is needed is when dealing with localized user groups.

12.5.2.1 Principal and identity

A principal object in .NET is an object that implements the IPrincipal interface, defined in the System.Security.Principal namespace as:

     public interface IPrincipal     {        IIdentity Identity{get;}        bool IsInRole(string role);     } 

The IsInRole( ) method returns true if the identity associated with this principal is a member of the specified role, and false otherwise. The Identity read-only property provides access to read-only information about the identity, in the form of an object implementing the IIdentity interface:

     public interface IIdentity     {        string AuthenticationType{get;}        bool IsAuthenticated{get;}        string Name{get;}     } 

Every .NET thread has a principal object associated with it, obtained via the CurrentPrincipal static property of the Thread class:

     public static IPrincipal CurrentPrincipal{get;set;} 

For example, here is how to obtain the username from the principal object:

     void GreetUser(  )     {        IPrincipal principal = Thread.CurrentPrincipal;        IIdentity  identity  = principal.Identity;        string greeting = "Hello " + identity.Name;        MessageBox.Show(greeting);     } 

12.5.2.2 Verifying role membership

Imagine a banking application that lets users transfer sums of money between two specified accounts. Only customers and tellers are allowed to call this method, with the following business rule: if the amount transferred is greater than 5,000, only tellers are allowed to do the transfer. Declarative role-based security can verify that the caller is a teller or a customer, but it can't enforce the additional business rule. For that, you need to use the IsInRole( ) method of IPrincipal, as shown in Example 12-12.

Example 12-12. Programmatic role membership verification
 using System.Security.Permissions; using System.Security.Principal; using System.Threading;   public class Bank {    const int MaxSum = 5000;       [PrincipalPermission(SecurityAction.Demand,Role =@"<domain>\Customers")]    [PrincipalPermission(SecurityAction.Demand,Role =@"<domain>\Tellers")]    public void TransferMoney(double sum,long sourceAccount,long destinationAccount)    {       IPrincipal  principal;       principal = Thread.CurrentPrincipal;       Debug.Assert(principal.Identity.IsAuthenticated);         bool isCustomer = false;       bool isTeller   = false;       isCustomer = principal.IsInRole(@"<domain>\Customers");       isTeller   = principal.IsInRole(@"<domain>\Tellers");       if(isCustomer && ! isTeller)//The caller is a customer not teller       {          if(sum > MaxSum)          {             string message = "Caller does not have sufficient authority to" +                              "transfer this sum";             throw new UnauthorizedAccessException(message);          }       }       DoTransfer(sum,sourceAccount,destinationAccount);    }    //Helper method    void DoTransfer(double sum,long sourceAccount,long destinationAccount)    {...} } 

Example 12-12 demonstrates a number of points. First, even though it uses programmatic role-membership verification with the value of the sum argument, it still uses declarative role-based security as the first line of defense, allowing access only to users who are members of the Customers or Tellers roles. Second, you can programmatically assert that the caller is authenticated using the IsAuthenticated property of IIdentity. Finally, in case of unauthorized access, you can throw an exception of type UnauthorizedAccessException.

12.5.3. Windows Security Principal

In a Windows application, the principal object associated with a .NET thread is of type WindowsPrincipal:

     public class WindowsPrincipal : IPrincipal     {        public WindowsPrincipal(WindowsIdentity ntIdentity);          //IPrincipal implementation        public virtual IIdentity Identity{ get;}        public virtual bool IsInRole(string role);        //Additional methods:        public virtual bool IsInRole(int rid);        public virtual bool IsInRole(WindowsBuiltInRole role);        public virtual bool IsInRole(SecurityIdentifier sid);     } 

WindowsPrincipal provides two additional IsInRole( ) methods that are intended to ease the task of localizing roles (i.e., Windows user groups). The first version takes an enum of type WindowsBuiltInRole matching the built-in Windows roles, such as WindowsBuiltInRole.Administrator or WindowsBuiltInRole.User. The other version of IsInRole( ) accepts an integer indexing specific roles. For example, a role index of 512 maps to the Administrators group. The MSDN Library contains a list of both the predefined indexes and ways to provide your own aliases and indexes to user groups. The default identity associated with the WindowsPrincipal object is an object of type WindowsIdentity that provides a number of methods beyond the implementation of IIdentity, including helper methods for verifying major user-group membership and impersonation. When asked to verify role membership, WindowsPrincipal retrieves the username from its identity object and looks it up in the Windows (or domain) user-group repository.

12.5.4. Custom Security Principal

There is a complete disconnection between declarative role-based security and the actual principal object type. When the PrincipalPermission attribute is asked to verify role membership, it simply gets hold of its thread's current principal object (in the form of IPrincipal) and calls its IsInRole( ) method. This disconnection is also true of programmatic role-membership verification that uses only IPrincipal, as shown in Example 12-13. The separation of the IPrincipal interface from its implementation is the key to providing role-based security mechanisms other than Windows user groupsall you need to do is provide an object that implements IPrincipal and set your current thread's CurrentPrincipal property to that object. In addition, code that installs a custom security principal must be granted permission to control security principals. Example 12-13 demonstrates installing a custom role-based security mechanism using a trivial custom principal.

Example 12-13. Implementing and installing a custom principal
 public class MyCustomPrincipal : IPrincipal {    IIdentity m_OldIdentity;    public MyCustomPrincipal(  )    {       m_OldIdentity = Thread.CurrentPrincipal.Identity;    }    public IIdentity Identity    {       get {return m_OldIdentity;}    }    public bool IsInRole(string role)    {       switch(role)       {          case "Authors":          {             if (m_OldIdentity.Name == "Juval")                return true;             else                return false;          }          default:             return false;       }    } } //Installing the custom principal: IPrincipal customPrincipal = new MyCustomPrincipal(  ); Thread.CurrentPrincipal = customPrincipal; 

In this example, the custom principal caches the current identity because it doesn't want to provide a new identity. The custom principal returns true from IsInRole( ) only if the role specified is "Authors" and the username is "Juval". Of course, a real-life custom principal also does some actual role-membership verification, such as accessing a dedicated table in a database. For example, Appendix B uses the ASP.NET custom principal in the context of a Windows Forms application, and that custom principal uses an SQL Server for storing roles and users. Fundamentally, however, the custom principal in Appendix B isn't much different from the custom principal in Example 12-13.

You have to install the custom security principal in every thread in your application that uses role-based security (either declaratively or programmatically), because by default .NET attaches the Windows principal to every new thread. Alternatively, you can provide .NET with a new default principal object to attach to new threads. To provide a new default principal, use the static method SetThreadPrincipal( ) of the AppDomain class. For example:

     IPrincipal customPrincipal = new MyCustomPrincipal(  );     AppDomain currentDomain = AppDomain.CurrentDomain;     currentDomain.SetThreadPrincipal(customPrincipal); 

Note that the new default is app domain-wide, and that you can't call SetThreadPrincipal( ) more than once per app domain. If you call it more than once, .NET throws an exception of type PolicyException.

Some applications can't use Windows user groups as roles and have no need for an elaborate custom principal. For such simple cases, you can use the GenericPrincipal class. Its constructor accepts the identity object to use and a collection of roles the identity is a member of. GenericPrincipal's implementation of IsInRole( ) simply scans that collection looking for a match.




Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

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