Tuesday 12 February 2013

Role Based Access Control

This introduction to aspects of Identity and Access Control in Microsoft .NET 1.0 through 4.0 serves as groundwork for examination, in a later article, of the same issues in .NET 4.5. We start with two short interface declarations:

IIdentity and IPrincipal

These two interfaces first appeared in 2002, with the initial release of .NET 1.0 (and ASP.NET 1.0):
interface IIdentity
{
  bool IsAuthenticated { get; }
  string AuthenticationType { get; }
  string Name { get; }
}

interface IPrincipal
{
  IIdentity Identity { get; }
  bool IsInRole(string roleName);
}
For developers these interfaces abstract, and their implementing classes encapsulate, the most important features of identity-related concerns. IIdentity deals only with authenticationName obviously identifies the user; AuthenticationType describes how that user became authenticated (e.g. via Windows Authentication or Kerberos, or some other custom mechanism); and IsAuthenticated indicates whether the user is actually authenticated at all (client-server apps sometimes allow anonymous users). IPrincipal extends this provision into the adjacent realm of authorization, by providing the single method IsInRole allowing the aggregated IIdentity to be interrogated at the first level of role-based access control granularity.

Thread.CurrentPrincipal

Not to be confused with the Win32 "thread token", this is a static property of the Thread class, giving access to the currently executing thread's IPrincipal implementation. So not strictly static then, as every thread in your system can have a different value of IPrincipal. More sort of thread-static. It is set up when the security context for a client is established, either by the startup code of a desktop app, or by some runtime in the case of an ASP.NET or WCF server app.

WindowsIdentity and WindowsPrincipal

When the AuthenticationType indicates Windows Authentication, these are the concrete System.Security.Principal classes used to wrap the Windows security token, encapsulating the Windows account of the current desktop app process (or of the client, in a client-server system):
// Get Windows account user name
var id = WindowsIdentity.GetCurrent();
var userName = id.Name;
// Check that user belongs to Builtin\Users
var principal = new WindowsPrincipal(id);
var isUser = principal.IsInRole(@"Builtin\Users");
Note that while this code works, it is not advisable to refer to user groups using strings. For one thing, the built-in group names are localized. For another, an administrator might change the name of a group, obviously breaking any hard-coded string based checks. For reasons like these, Windows internally uses not strings but immutable Security IDs (SIDs) to reference both users and groups:
var account = new NTAccount(userName);
Console.WriteLine(account); // MY-DOMAIN\johnk

var sid = account.Translate(typeof(SecurityIdentifier));
Console.WriteLine(sid); // S-1-5-21-29562463-4174942564-1615450050-1377

foreach (var group in id.Groups)
  Console.WriteLine(group.Translate(typeof(NTAccount)));
This last line prints the list of groups as human-readable strings, e.g.,
MY-DOMAIN\Domain Users
Everyone
BUILTIN\Users
NT AUTHORITY\INTERACTIVE
CONSOLE LOGON
NT AUTHORITY\Authenticated Users
NT AUTHORITY\This Organization
LOCAL
MY-DOMAIN\Local Group
MY-DOMAIN\Employment House
MY-DOMAIN\Development
Remove the Translate call to see the raw security IDs underlying your group names.

Returning to the earlier question about the interrogation of roles, we can now use the idea of a security identifier, together with the provided enumeration WellKnownSidType, to determine for example whether the current user is a local administrator:
var localAdmins = new SecurityIdentifier(
  WellKnownSidType.BuiltinAdministratorsSid, null);
Console.WriteLine(principal.IsInRole(localAdmins));
The second parameter to the above SecurityIdentifier constructor is itself a SID. As suggested by its parameter name domainSid, domains also have security IDs, and can be passed around as such when required. For example, to determine whether the user is a domain administrator, you have to supply the SID of the relevant domain, which in this case is the user's account domain:
var domainAdmins = new SecurityIdentifier(
  WellKnownSidType.BuiltinAdministratorsSid, id.User.AccountDomainSid);
Console.WriteLine(principal.IsInRole(domainAdmins));
Lastly, to answer the original question about whether the user is a member of the built-in users group,
var users = new SecurityIdentifier(
  WellKnownSidType.BuiltinUsersSid, null);
Console.WriteLine(principal.IsInRole(users));
UAC Reminder

Ah, yes. Do remember when interpreting these results client side, that the returned values respect User Account Control (UAC), enabled by default since Windows Vista. UAC prefers to run client processes without administrative privileges, unless instructed otherwise (e.g. by Run as Administrator).

GenericIdentity and GenericPrincipal

So much for the WindowsIdentity and WindowsPrincipal classes. These are ideal when your user list is stored in Windows itself, for example when you use Active Directory. But what if your user list is in some other repository, such as a SQL database?

Using the GenericIdentity and GenericPrincipal classes, you can manage user names and roles for yourself, persisting them in your own custom credential store, while still taking advantage of those very useful IIdentity and IPrincipal properties - and specifically, the IsInRole method. Using System.Threading, we have (assuming that Alice has previously supplied a correct password, or otherwise authenticated):
var id = new GenericIdentity("alice");
var roles = new[] { "Sales", "Marketing" }
var user = new GenericPrincipal(id, roles);
Thread.CurrentPrincipal = user;
This sets up the necessary principal for subsequent use. Later - say, during the execution of some other method on this thread - we might want to find out the current user name, and whether she is in Sales:
var user = Thread.CurrentPrincipal;
Console.WriteLine(user.Identity.Name); // alice
Console.WriteLine(user.IsInRole("Sales")); // True
Authorization and Roles

Whichever IIdentity and IPrincipal implementations we use, the mechanisms of role-based authorisation are the same. Here are four alternatives, from the imperative to the declarative, all of which resolve directly or eventually into IsInRole calls:
  1. make direct calls to Thread.CurrentPrincipal.IsInRole();
  2. assert System.Security.Permissions.PrincipalPermission demands;
  3. apply [PrincipalPermission] attributes to methods or classes;
  4. add <authorization /> elements to web.config.
Example 1 - coded tests:
if (user.IsInRole("Marketing"))
  DoSomeMarketing();
else
  AccessDenied();
Example 2 - security assertions in code:
// Throws a SecurityException if the assertion fails
new PrincipalPermission(null, "Marketing").Demand();
Example 3 - security assertions in attributes:
[PrincipalPermission(SecurityAction.Demand, Role = "Sales")]
[PrincipalPermission(SecurityAction.Demand, Role = "Marketing")]
private static void DoSomeSalesOrMarketing()
{
  // ...
}
Example 4 - <authorization /> elements in web.config:
<configuration>
   <system.web>
      <authorization>
         <allow roles="Sales,Marketing"/>
         <deny users="*"/>
      </authorization>
   </system.web>
</configuration>
Notice that options 1-3 above all have the disadvantage that the security model is hard coded throughout the source. Obviously, this can be very hard to maintain. Also in option 3, there is no simple, convenient and tidy way to hide these security assertions from the CLR, for example to run unit tests. These objections and many others have been addressed by the claims-based security authorization model introduced in .NET 4.5, which is the subject of the next article in this series.

No comments:

Post a Comment