.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.
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 article all use Windows user
groups, but .NET allows you to provide your own custom role definitions.
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.
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. 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. |
|
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. |
|
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.
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);
}
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 1.
Example 1. 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 1 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.
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.
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 groups—all 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 2 demonstrates installing a custom role-based security mechanism using a trivial custom principal.
Example 2. 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.
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. |