Download the code for this article: RollSec.exe (161KB) |
SUMMARY Role-based security allows administrators to assign access permissions to users based on the roles they play rather than on their individual identities. These privileges can be used to control access to objects and methods, and are easier to identify and maintain than user-based security. The .NET Framework provides two role-based security models, which are exposed as two namespaces: System.Enterprise-Services and System.Security.Permissions. Presented here is a comparison of the two options and a discussion of when each is the right choice. The author also demonstrates the process involved in setting up access security and discusses role memberships. |
ole-based security is by far the most elegant and productive way to provide user authorization and access checks for your application. A role is a category of users who share the same security privileges. When you grant a given role access to an object or a particular method on that object, you grant it access to the members of that role, freeing you from having to assign specific access rights to individual users. Discovering the roles users play in your business domain is part of your application requirement analysis and design, just as factoring components and interfaces is. Using roles instead of particular users means you don't have to change your application when changes occur in real life, such as new users being added, existing users moving between positions, users being promoted, or users leaving their jobs. Microsoft?.NET allows developers to use role-based security as attributes, freeing the business logic from having to deal with security access checks. In fact, .NET offers two role-based security models. This article's downloadable sample code contains a handy utility that unifies the two security models, allowing you to get the best of both models without compromising on ease of use or having the components using basic role-based security do anything differently. I've used some advanced and valuable C# programming techniques such as language features and reflection in building this utility. Role-based Security Models The first role-based .NET Security model is part of the vast component-oriented permission and evidence based security infrastructure built into .NET. It's available in the System.Security.Permissions namespace. To understand this first model, imagine a bank account component that provides methods for opening an account and retrieving the account's balance. As part of the requirements analysis, you discovered that only tellers can open an account, but that both tellers and customers can retrieve the account's balance. To implement this requirement, use the PrincipalPermission attribute, specifying the roles you grant access to and the security action to perform. In this case, the security action specified (SecurityAction.Demand) needs to verify the caller's role membership during call time, before allowing the call to go through: using System.Security.Permissions; public class BankAccount { [PrincipalPermission(SecurityAction.Demand,Role="Teller")] public long OpenAccount(){...} [PrincipalPermission(SecurityAction.Demand,Role="Customer")] [PrincipalPermission(SecurityAction.Demand,Role="Teller")] public long GetBalance(){...} /* Rest of the implementation */ }By default, a security role in .NET is a Windows?user group. In my example, only if the caller is a member of the Teller Windows user group will it be allowed to call the OpenAccount method. The second role-based security model is part of .NET Enterprise Services and is defined in the System.EnterpriseServices namespace. .NET Enterprise Services are actually the result of integrating COM+ component services with .NET. They offer a wide range of component services essential for any nontrivial application: object pooling; instance management; transactions; concurrency management; security; loosely coupled events; and asynchronous, disconnected calls. In order to implement the same requirement as in the previous example using .NET Enterprise Services, derive the BankAccount class from ServicedComponent (components that use .NET Enterprise Services are called serviced components), and use the SecurityRole attribute: using System.EnterpriseServices; public class BankAccount : ServicedComponent { [SecurityRole("Teller")] public long OpenAccount(){...} [SecurityRole("Customer")] [SecurityRole("Teller")] public long GetBalance(){...} /* Rest of the implementation */ }An Enterprise Services role is defined in the Enterprise Services Catalog in the application that the serviced component is part of. Typically, you would use the Component Services Explorer (also known as the COM+ Explorer) to define these roles, but you can also use the SecurityRole attribute as an assembly attribute to define them: [assembly: SecurityRole("Teller")] [assembly: SecurityRole("Customer")]Then use the Component Services Explorer to add users (or user groups) to these roles. Figure 1 shows the Bank App application with the Teller and Customer roles. Figure 1 Teller and Customer Roles Comparing the Two Models The two role-based security models complement each other, and neither is superior in all respects. The main drawback of the .NET basic role-based security model is that it can be only as granular as the user groups in the hosting domain. Often you do not have control over your customer's IT department. If you deploy your application in an environment where the user groups are coarse, where the user groups do not map well to actual roles users play in your application, or where the group names are slightly different, then .NET basic role-based security is of little use to you. The Enterprise Services role-based security is in principle unrelated to Windows user groups, allowing you to define roles directly from the application's business domain, even if no corresponding user groups exist. Of course, you can associate actual user groups with Enterprise Services roles if such groups exist. On the other hand, .NET basic role-based security offers a rich set of security actions to perform in conjunction with the role membership information. The SecurityAction enumeration provides other values besides SecurityAction.Demand, such as SecurityAction.LinkDemand, to verify role-membership during the JIT compilation link phase instead of on every call. SecurityAction.LinkDemand may be more useful to you if it is always the same client calling your object, and it will yield better performance in certain intense calling patterns. See Keith Brown's article "Security in .NET: Enforce Code Access Rights with the Common Language Runtime" (MSDN Magazine, February, 2001) for more information on possible SecurityAction values. Another advantage to the basic security model is that it imposes no restrictions on the base class, unlike Enterprise Services, which requires deriving from ServicedComponent. A typical enterprise application will almost certainly be comprised of both serviced components and regular components. It simply doesn't make any sense having two security models for the same application with two separate definitions of what roles are and separate allocation of users to those roles. Solution Architecture Like most other pieces of the .NET Framework, the security infrastructure is extensible, and you can pretty much plug in your own implementation of any key element of it. In fact, it is surprisingly easy to do so, and you will see that plugging in your implementation takes only two or three lines of code. In .NET, user information, and in particular user identity and role membership, is represented by the IPrincipal interface defined in the System.Security.Principal namespace: public interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string role); }Every .NET thread has a principal object associated with it. You can access the principal object via the thread class's static CurrentPrincipal property, which returns an IPrincipal object: IPrincipal principal = Thread.CurrentPrincipal;When the PrincipalPermission attribute needs to verify that the caller is in the specified role, .NET simply retrieves the principal object from the current thread and calls IsInRole. Nothing in the PrincipalPermission attribute implementation pertains to how role membership verification is made. The default implementation of IsInRole, when used with the principal object, looks up the Windows user group named as the role and verifies that the caller is a member of that group. The caller is represented by an object that implements the IIdentity interface defined as follows: public interface IIdentity { string AuthenticationType { get; } bool IsAuthenticated { get; } string Name { get; } }Every principal object has an identity object associated with it, accessible via the read-only Identity property of the principal object. The caller's user name is accessible via the read-only Name property of the identity object. As you can see, there is complete separation between the PrincipalPermission attribute and the principal object. To unify the two role-based security models, all I needed to do was provide my own custom security principal. I call this custom principal the UnifiedPrincipal. The implementation of IsInRole in UnifiedPrincipal can verify the caller is a member of an Enterprise Services role instead of a Windows group. In fact, UnifiedPrincipal lets you choose the security model to use. You can use Enterprise Services only, Windows groups only, both, or either. Using both means the calling user must be a member of both an Enterprise Services role and a matching Windows user group. Using either means as long as the user is a member of either one of the role definitions, IsInRole will return true. The UnifiedPrincipal class public members are listed in Figure 2. (See the code download for this article at the link at the top of this article to get the UnifiedSecurity solution that contains the UnifiedPrincipal class library and a test client application). Using the unified security principal requires adding only one line of code to every app domain for selecting the security model. Typically, you would add this line to your application's Main: static void Main() { UnifiedPrincipal.SetModel(SecurityRoleModel.EnterpriseServices); /* Rest of Main() */ }But even if you develop a class library, you can call SetModel in your component's code. As you will see later on, there is no harm in calling SetModel multiple times. In addition, neither the client nor the server assemblies need to be part of an Enterprise Services application or contain serviced components. As you know, C# does not support default parameter values. You can simulate them, however, by overloading methods. UnifiedPrincipal has overloaded SetModel methods, providing you with four ways of specifying the security model to use and the Enterprise Services application name containing the role. The default model is SecurityRoleModel.EnterpriseServices. That's all you need to know to use UnifiedPrincipal. Now let's see how the UnifiedPrincipal is implemented. Applications and Assemblies The UnifiedPrincipal has to know the name of the Enterprise Services application in which to look for the role and the user membership. If the assembly calling SetModel provides an application name as an argument, then that name is used. But what should UnifiedPrincipal do if no name is provided? In general, when adding an assembly to an Enterprise Services application, you have a number of options for specifing the application name. You can use the AssemblyName assembly attribute: [assembly: ApplicationName("MyApp")]If no name is provided, .NET uses the assembly name for the application name. Even though the assembly using UnifiedPrincipal may not contain serviced components at all, it makes sense to follow the rules in case it does. When an assembly uses the UnifiedPrincipal's SetModel method without providing an application name, the UnifiedPrincipal will use reflection to look for the ApplicationName attribute in the calling assembly. If none is found, it will use the calling assembly name for the application name. UnifiedPrincipal has a helper method called GetAppNameFromAssembly, shown in Figure 3. GetAppNameFromAssembly accepts an assembly object representing the calling assembly as an argument. It then retrieves an array of attributes of type ApplicationName associated with that assembly. A given assembly can have at most one such attribute, so the array size must be zero or one. You can discover the array size using the Length property of any C# array object. If the array size is zero (no attribute available), then the assembly name is returned using the GetName method of the assembly object. Note that GetName doesn't return a string, but a strongly typed AssemblyName object. You can get the human-readable assembly name by accessing the AssemblyName's Name property. If the assembly does contain an AssemblyName attribute, then the attribute's Value property is the requested application name. Simple enough. But then how would UnifiedPrincipal get an object representing the calling assembly in the first place? Luckily, the Assembly type provides the static method GetCallingAssembly, which returns an assembly object representing the calling assembly immediately up the call chain. So the SetModel versions that do not accept the application name as an argument use GetCallingAssembly to get the calling assembly object, and then call GetAppNameFromAssembly: static public void SetModel(SecurityRoleModel model) { Assembly callingAssembly = Assembly.GetCallingAssembly(); string appName = GetAppNameFromAssembly(callingAssembly); SetModel(appName,model); }Installing a Custom Security Principal I wanted to prevent the user from instantiating a UnifiedPrincipal object directly. UnifiedPrincipal is mostly for .NET use, and the stability of all the objects using the PrincipalPermission attribute depends on it. It is common practice in .NET in similar cases to provide only public static methods, and have the static methods internally use a protected or private constructor to create the object. This is exactly what the fully parameterized SetModel method does: static public void SetModel(string appName,SecurityRoleModel model) { UnifiedPrincipal principal = new UnifiedPrincipal(model); principal.AppName = appName; }SetModel creates a new UnifiedPrincipal object whose constructor does the actual work of installing itself as a custom principal. The constructor code is shown in Figure 4. Before you can do anything meaningful with .NET role-based security, you have to set your current app domain principal policy. The available options are no principal (not useful for you because you want to install and use a principal), unauthenticated principal (not much use for role-based security in general), and WindowsPrincipal. This is exactly what the first two lines of the constructor do: AppDomain currentDomain = Thread.GetDomain(); currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);You get your current app domain using the Thread class's GetDomain static property. Next, the constructor checks the constructor argument梩he security model. If it's SecurityRoleModel.WindowsGroups, then there is nothing to unify, and the default .NET behavior is just fine, so the constructor simply exits. If a unified model is requested (Enterprise Services, both, or either), then the constructor saves the requested model value as well as the current principal and identity objects (you will see why shortly). To actually install a custom principal, all you have to do is assign the CurrentPrincipal property of the current thread: Thread.CurrentPrincipal = this;In essence you are finished. You still have to repeat the assignment on every new thread created in this application domain in order to override the .NET default principal. Yet again, .NET provides an easy remedy via the application domain object's SetThreadPrincipal method. When you call this method, you provide .NET with a new default principal to use for all new threads. You can only call SetThreadPrincipal once per application domain, and you must verify that you called it only once. Therefore, the constructor compares the type of the default principal with UnifiedPrincipal using the C# is operator, and proceeds to call SetThreadPrincipal only if the type of the default principal is not UnifiedPrincipal: if(m_DefaultPrincipal is UnifiedPrincipal == false) { currentDomain.SetThreadPrincipal(this); }Role Membership Lookup UnifiedPrincipal must provide the IPrincipal's IsInRole method. To implement IsInRole, UnifiedPrincipal has two helper methods: IsInWindowsGroup and IsInEnterpriseServicesRole. UnifiedPrincipal uses these helper methods according to the value of the requested security model, passed as a constructor argument, and saved in m_Model. Figure 5 shows the implementation of IsInRole. Implementing IsInWindowsGroup is trivial once you realize that this is exactly the default .NET principal behavior protected bool IsInWindowsGroup(string group) { return m_DefaultPrincipal.IsInRole(group); }and this is why the constructor saved the default principal. IsInEnterpriseServicesRole is where UnifiedPrincipal has to find the role in the Enterprise Services Catalog and look up the user in that role. The Enterprise Services Catalog provides a generic iteration programming model. Folders in the Component Services Explorer are represented by Catalog Collections. Individual items in a Collection are represented by Catalog Objects. All Catalog Collections provide the ICatalogCollection interface, which allows you to iterate over individual Catalog Objects. And all Catalog Objects provide the ICatalogObject interface, which allows you to access named properties. The Catalog itself is accessible by creating an object of type COMAdminCatalog, which provides the ICOMAdminCatalog interface and serves as the root of the Catalog. The Catalog type definitions are not available out-of-the-box in .NET. You must import the COM+ 1.0 Admin Type Library using the COM tab of the Add Reference dialog box. Once imported, the Catalog types are available under the COMAdmin namespace. There are quite a few details to master when programming against the Catalog. In a nutshell, by using ICOMAdminCatalog, UnifiedPrincipal gains access to the Applications Collection where it scans the items (application objects) searching for a match against the application name. Once it finds the application, it then accesses its Roles Collection. The Roles Collection has one Catalog Object per role. UnifiedPrincipal scans the Roles Collection searching for the specified role. Once found, it accesses the role's UsersInRole Collection. UsersInRole contains the users associated with that role as shown in Figure 6. Figure 6 Navigating the Component Services Catalog Recall that the UnifiedSecurity constructor cached the existing IIdentity object. This identity object provides the Name property. All UnifiedPrincipal has to do is compare each user Catalog Object in UsersInRole with the identity's name. If a match is found, then the caller is a member of the specified role. Figure 7 shows the implementation of IsInEnterpriseServicesRole with error handling removed for clarity. Wrap-up You've seen how easy it is to extend a core building block of .NET and plug a custom block with literally one line of code. Accomplishing the same functionality in Windows would have required serious programming, not to mention considerable in-depth security expertise. This is no surprise because Windows was never meant for such extensibility, while .NET was designed with this in mind. Even though the implementation details of this custom principal are specific to the problem at hand (unifying .NET role-based security models), learning how to provide a custom security principal is useful in a number of other scenarios. Imagine an ASP.NET application that maintains a user/password/role database. You can write a custom principal to look up role membership in the database and still use the PrincipalPermission attribute. In general, writing a custom principal is an important technique for using the same business components code with different authorization implementations. |
For related articles see: Security in .NET: Enforce Code Access Rights with the Common Language Runtime Security Briefs: Managed Security Context in ASP.NET http://www.discuss.develop.com/dotnet.html For background information see: Version 1 Security Changes for the Microsoft .NET Framework |
Juval Lowy is a seasoned software architect providing consulting and training on .NET design and .NET migration. This article contains adaptations from his book COM and .NET Component Services (O'Reilly, 2001). Contact Juval at http://www.idesign.net. |
Figure 2 UnifiedPrincipal Public Members public enum SecurityRoleModel { WindowsGroups, EnterpriseServices, Either, Both } public class UnifiedPrincipal : IPrincipal { static public void SetModel(){} static public void SetModel(string appName){} static public void SetModel(SecurityRoleModel model){} static public void SetModel(string appName,SecurityRoleModel model){} //IPrincipal methods: public IIdentity Identity { get; } public bool IsInRole(string role); /* Rest of class definition: protected methods and members */ } Figure 3 The GetAppNameFromAssembly Method public static string GetAppNameFromAssembly(Assembly assembly) { Type AttributeType = typeof(ApplicationNameAttribute); object[] objArray = assembly.GetCustomAttributes(AttributeType,true); //One ApplicationName attribute is allowed at most Debug.Assert(objArray.Length == 1 || objArray.Length == 0); if(objArray.Length == 0) { //In the absence of ApplicationName attribute, assembly name is //used AssemblyName assemblyName = assembly.GetName(); return assemblyName.Name; } ApplicationNameAttribute appNameAttribute; appNameAttribute = (ApplicationNameAttribute)objArray[0]; return appNameAttribute.Value; } Figure 4 Installing the Custom Principal public class UnifiedPrincipal : IPrincipal { protected string m_AppName; protected IIdentity m_Identity; protected IPrincipal m_DefaultPrincipal; protected SecurityRoleModel m_Model; protected UnifiedPrincipal(SecurityRoleModel model) { AppDomain currentDomain = Thread.GetDomain(); currentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal); if(model == SecurityRoleModel.WindowsGroups) { return;// Don't do anything, default is fine } m_Model = model; m_AppName = ""; //Save old principal m_DefaultPrincipal = Thread.CurrentPrincipal; //use current identity m_Identity = Thread.CurrentPrincipal.Identity; //Make us the principal for this thread Thread.CurrentPrincipal = this; //Make sure all future threads in this app domain use this //principal but because default principal cannot be set twice: if(m_DefaultPrincipal is UnifiedPrincipal == false) { currentDomain.SetThreadPrincipal(this); } } /* Rest of the class definition */ } Figure 5 Implementing IsInRole public bool IsInRole(string role) { switch(m_Model) { case SecurityRoleModel.Either: { return IsInWindowsGroup(role) || IsInEnterpriseServicesRole(role); } case SecurityRoleModel.EnterpriseServices: { return IsInEnterpriseServicesRole(role); } case SecurityRoleModel.Both: { return IsInWindowsGroup(role) && IsInEnterpriseServicesRole(role); } default: { Debug.Assert(false); return false; } } } Figure 7 Implementing IsInEnterpriseServicesRole using COMAdmin; public class UnifiedPrincipal : IPrincipal { protected string m_AppName; protected IIdentity m_Identity; protected SecurityRoleModel m_Model; protected bool IsInEnterpriseServicesRole(string role) { bool inRole = false; string userName = m_Identity.Name; //Find application ICOMAdminCatalog catalog; ICatalogCollection applicationCollection; ICatalogObject application = null; int applicationCount; int appIndex = 0; catalog = (ICOMAdminCatalog)new COMAdminCatalog(); applicationCollection = (ICatalogCollection)catalog.GetCollection("Applications"); //Read the information from the catalog applicationCollection.Populate(); applicationCount = applicationCollection.Count; string tempName =""; while(tempName != m_AppName && appIndex < applicationCount) { //Get the current application application= (ICatalogObject)applicationCollection.get_Item (appIndex++); tempName = application.Name.ToString(); } object appKey = application.Key; //Get Roles collection ICatalogCollection roleCollection; roleCollection = (ICatalogCollection)applicationCollection.GetCollection("Roles",appKey); roleCollection.Populate(); int roleIndex = 0; while(inRole == false && roleIndex <roleCollection.Count) { //Get individual role ICatalogObject roleObj; roleObj = (ICatalogObject)roleCollection.get_Item(roleIndex); if(roleObj.Name.ToString() != role) { roleIndex++; continue; } //Role name match. get users collection, and check each user object roleKey = roleObj.Key; ICatalogCollection userCollection; userCollection = (ICatalogCollection)roleCollection.GetCollection ("UsersInRole",roleKey); userCollection.Populate(); int userIndex = 0; while(inRole == false && userIndex <userCollection.Count) { //Get individual user object ICatalogObject user; user = (ICatalogObject)userCollection.get_Item(userIndex); //for each user, get users name, and compare if (userName == user.Name.ToString()) { inRole = true; break; } //User in a role can actually be a user group. Check membership //by using generic principal, that considers user group as //"role" inRole = IsInWindowsGroup(user.Name.ToString()); userIndex++; } roleIndex++; } return inRole; } /* Rest of the class definition */ } |
本文地址:http://com.8s8s.com/it/it45794.htm