IIdentity and IPrincipal
These two interfaces first appeared in 2002, with the initial release of .NET 1.0 (and ASP.NET 1.0):
For developers these interfaces abstract, and their implementing classes encapsulate, the most important features of identity-related concerns. IIdentity deals only with authentication: Name 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.interface IIdentity { bool IsAuthenticated { get; } string AuthenticationType { get; } string Name { get; } } interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string roleName); }
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):
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:// 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");
This last line prints the list of groups as human-readable strings, e.g.,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)));
Remove the Translate call to see the raw security IDs underlying your group names.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
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:
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 localAdmins = new SecurityIdentifier( WellKnownSidType.BuiltinAdministratorsSid, null); Console.WriteLine(principal.IsInRole(localAdmins));
Lastly, to answer the original question about whether the user is a member of the built-in users group,var domainAdmins = new SecurityIdentifier( WellKnownSidType.BuiltinAdministratorsSid, id.User.AccountDomainSid); Console.WriteLine(principal.IsInRole(domainAdmins));
UAC Remindervar users = new SecurityIdentifier( WellKnownSidType.BuiltinUsersSid, null); Console.WriteLine(principal.IsInRole(users));
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):
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 id = new GenericIdentity("alice"); var roles = new[] { "Sales", "Marketing" } var user = new GenericPrincipal(id, roles); Thread.CurrentPrincipal = user;
Authorization and Rolesvar user = Thread.CurrentPrincipal; Console.WriteLine(user.Identity.Name); // alice Console.WriteLine(user.IsInRole("Sales")); // True
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:
- make direct calls to Thread.CurrentPrincipal.IsInRole();
- assert System.Security.Permissions.PrincipalPermission demands;
- apply [PrincipalPermission] attributes to methods or classes;
- add <authorization /> elements to web.config.
Example 2 - security assertions in code:if (user.IsInRole("Marketing")) DoSomeMarketing(); else AccessDenied();
Example 3 - security assertions in attributes:// Throws a SecurityException if the assertion fails new PrincipalPermission(null, "Marketing").Demand();
Example 4 - <authorization /> elements in web.config:[PrincipalPermission(SecurityAction.Demand, Role = "Sales")] [PrincipalPermission(SecurityAction.Demand, Role = "Marketing")] private static void DoSomeSalesOrMarketing() { // ... }
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.<configuration> <system.web> <authorization> <allow roles="Sales,Marketing"/> <deny users="*"/> </authorization> </system.web> </configuration>
No comments:
Post a Comment