.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

8 Principal Based Security

Much of Windows security is principal based. In other words, principals (users or groups of users) are authorised to perform some action. When a Windows secured object is requested to perform some privileged operation it will obtain the principal and determine whether it is in the list of principals that are allowed to perform the operation (or conversely, whether it is in the list of principals explicitly denied the ability to perform the operation).

You've seen over the last few pages that the new innovation in .NET is code access security, that is, the runtime gives code permissions based on evidence (typically its source) rather than the identity of the user running the code. However, .NET security is layered over Win32 security so if you access an NT secured object your code will be still subject to a Windows access check. Page Two of this workshop illustrated this, it showed that you can use NT file access control lists to specify who can access a particular file. 

.NET Version 3.0
Version 3.0/2.0 of the framework library provides classes to manipulate NT ACLs and classes that are wrappers arround NT secure objects (like events and mutexes) provide methods to access their ACLs. The next page covers the code to do this.

In addition, .NET allows you to perform another type of access check which is implemented with .NET's permission attributes: principal based security checks. In this module I will explain how to use .NET principal security, how it is implemented and some of its weaknesses.

8.1 Principals

A principal is some identifier that is applied to a thread. You get to chose the type of principal that is used by applying principal policy by calling SetPrincipalPolicy on the current application domain. This method takes an enumeration called PrincipalPolicy:

[Serializable]
public enum PrincipalPolicy
{
   NoPrincipal;
   UnauthenticatedPrincipal;
   WindowsPrincipal;
}

When an application domain is created the default policy is UnauthenticatedPrincipal, that is, the name of the principal is the empty string. This comes as a shock to most people because when you run a .NET application on Windows you know that your application will have a Windows principal (which are thread based) and hence you expect that you will be able to get this Windows principal by default. Well, this is not the default, to get your Windows principal you have to use this code:

using System;
using System.Threading;
using System.Security.Principal;

class App
{
   static void Main()
   {
      AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
      Console.WriteLine("Thread Principal: {0}", Thread.CurrentPrincipal.Identity.Name);
   }
}

Here I have to remind you of Heisenberg's Uncertainty Principal, which effectively says that when you measure a value you affect the value that you have measured. This is the case with the static member Thread.CurrentPrincipal. When the thread starts this property will not have a value. The first time that you read this property it will check a static field for this value and if the field is non-null its value is returned. If the static field is null, then the property will access the thread's principal via the thread's call context and cache this in the static field. This means that the first value returned from CurrentPrincipal will be the value that will be always returned by this thread, regardless of the principal policy. Compile this code:

csc policy.cs

On my machine, the this code returns:

Thread Principal: MARS\RichardGrimes

This indicates the local account RichardGrimes on my machine, MARS. However, if I read the principal before changing the principal policy, like this:

Console.WriteLine("Thread Principal: {0}", Thread.CurrentPrincipal.Identity.Name);
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Console.WriteLine("Thread Principal: {0}", Thread.CurrentPrincipal.Identity.Name);

then the results will be:

Thread Principal:
Thread Principal:

The first call reads the principal when the policy is set to UnauthenticatedPrincipal and so an empty string is returned for the principal name. This principal is cached in the thread so that when the principal policy is changed the same principal is returned. Remove the first call to CurrentPrincipal.

It is important to note that you should call SetPrincipalPolicy as early as possible in your thread procedure, you should only call it once and you should not access the thread's principal before you have set the principal policy.

You can write your own custom principal-based security and to do this you must have authenticated users. As you can see from PrincipalPolicy the only value that implies authenticated users is WindowsPrincipal, so in effect, if you want to do anything with principals (including writing your own principals) you must set the principal policy to WindowsPrincipal. It would have been better if this enumeration had been called AuthenticatedPrincipal.

A principal implements the IPrincipal interface, which has two members:

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

IsInRole is used to check whether the current principal is in a particular role, which I will cover in a moment. The Identity member is an object that implements this interface:

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

As you can see these are read-only properties that allows you to determine whether the principal is authenticated, what type of authentication is used and the principal name.

There are two types of principals (and identities) in the framework library, and you are free to write your own. The two standard types are GenericPrincipal (and GenericIdentity) and WindowsPrincipal (and WindowsIdentity).As the names suggest GenericPrincipal is a generic implementation that you can use for your own custom principal policy, WindowsPrincipal is based on the principal object provided by Windows.

WindowsIdentity has some interesting methods. The static GetCurrent will return the Windows user for the current thread, and unlike Thread.CurrentPrincipal, this value is read afresh every time. To test out GetCurrent, add the following line:

Console.WriteLine("Windows Principal: {0}", WindowsIdentity.GetCurrent().Name);
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Console.WriteLine("Thread Principal: {0}", Thread.CurrentPrincipal.Identity.Name);

The results from this are:

Thread Principal: MARS\RichardGrimes
Thread Principal: MARS\RichardGrimes

GetCurrent is called before calling SetPrincipalPolicy because it is not dependent upon the principal policy.

.NET Version 3.0
In version 3.0/2.0 if the thread has an impersonation token then by default GetCurrent returns the identity of the principal token, not the impersonation token. However, there is an overload that takes a bool and if this is true then GetCurrent will return a WindowsIdentity only if the thread is impersonating a user.

The Impersonate method allows the current thread to impersonate another Windows user. However, to do this you must have access to an impersonation token, and the current version of the framework does not allow you to gain direct access to such a token, instead you need to use Platform Invoke to call LogonUser and your account must have the SE_TCB_NAME privilege.

.NET Version 3.0
It is appropriate at this point to mention the Process class in System.Diagnostics. The Start method on this class allows you to create a new process. In version 3.0/2.0 of the framework two overloads of this method allow you to provide the name and password for a user and the method will start the process under that user.

Note that WindowsPrincipal.GetCurrent will always return the identity of the logged on user, so if you impersonate another user GetCurrent will return information about the new user. This contrasts with Thread.CurrentPrincipal which, as I have already mentioned, will return a cached value regardless of whether you have impersonated another user. Furthermore, the thread principal will only return whatever value it was set to hold.

The Thread.CurrentPrincipal property is read/write. This means that you are perfectly free to set it to another value. Remove the call to GetCurrent and add the following line:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(@"MARS\Administrator"), null);
Console.WriteLine("Principal: {0}", Thread.CurrentPrincipal.Identity.Name);

This code will show that the current thread is running under my machine's Administrator account. However, this is not the case because the thread principal cannot perform Win32 thread impersonation. Clearly thread principals can return some confusing values and you should remember this when you principals in .NET role based security.

The WindowsIdentity class has several properties that give you information about the principal. IsAnonymous determines if the user is an anonymous account which is typically the case with ASP.NET applications, conversely, a Windows account will be authenticated and you can test for this through IsAuthenticated and the type of authentication will be returned through AuthenticationType. Furthermore, IsGuest indicates if the user is a guest account and IsSystem determines if the account is defined by the system. 

.NET version 3.0/2.0 adds some extra classes and members (which will be covered in detail on the next page). WindowsIndentity has a member called User which has the SID for the account. It also has Owner which is the owner of the security token. The SID is represented by a class called SecurityIdentifier. A SID is a binary value and you can get a byte array with the SID by calling GetBinaryForm. You can test the SID type by calling IsWellKnown passing a value from the WellKnownSidType enumeration to see if the SID is an Administrator, a guest, a local service, or any of the gamut of SID types. The WindowsIndentity also has a Group property which is a collection of IdentityReference objects. IdentityReference is an abstract class, and the collection actually contains the child class NTAccount. This has a method called Translate that you can use to obtain the SID.

For example, to get the SID for your account change the previous code to this:

static void Main()
{
   WindowsIdentity identity = WindowsIdentity.GetCurrent();
   NTAccount account = new NTAccount(identity.Name);
   Console.WriteLine(account);
   SecurityIdentifier sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
   Console.WriteLine(sid);
}

The SecurityIdentifier also has a Translate method that you can use to convert a SID into an NTAccount (and hence to a human readable form). Change the code to the following:

WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine("{0} is in: ", identity.Name);
foreach (SecurityIdentifier sid in identity.Groups)
{
   NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount));
   Console.WriteLine("\t" + account);
}

On my machine I get the following:

MARS\RichardGrimes is in:
   MARS\None
   Everyone
   MARS\Debugger Users
   BUILTIN\Administrators
   BUILTIN\Users
   NT AUTHORITY\INTERACTIVE
   NT AUTHORITY\Authenticated Users
   LOCAL

8.2 Roles

Roles are essentially named 'capabilities' of principals. There are two ways to use them. Firstly, Windows security has a concept called aliases or groups. These are one or more security accounts and Windows security can grant (or deny) access to a group which will apply that grant or deny to all users in the group. Each Windows user is a member of one or more group because every user is a member of at least the Users group. If you use PrincipalPolicy.WindowsPrincipal then .NET role based security will map Windows groups to .NET roles, so if you are a member of the Administrators group .NET will treat you as being part of the BUILTIN\Administrators role. (The last example in the last example shows how you can get access to NT groups with .NET version 3.0/2.0.) The other way to use roles is to define your own, and to do this you need to create a custom principal object.

.NET Version 3.0
Role based security is based on checking that a principal is in a role. IPrincipal.IsInRole takes a string therefore the role must have a string name, thus WindowsPrincipal will perform role checks against role names. .NET version 3.0/2.0 adds a new version of this method to WindowsPrincipal that takes a SecurityIdentifier so a role check can be performed against a SID.

First, lets try using role base permissions with Windows accounts. As with code access security you have a choice of using imperative security by creating an object and making a demand, or declarative security by adding an attribute to a class or a method. For example, declarative security is as simple as applying a few attributes:

using System;
using System.Security.Principal;
using System.Security.Permissions;

class App
{
   static void Main()
   {
      // see later
   }
   [PrincipalPermission(SecurityAction.Demand, Role=@"BUILTIN\Administrators")]
   static void OnlyAdministrators()
   {
      Console.WriteLine("OnlyAdministrators called");
   }
   [PrincipalPermission(SecurityAction.Demand, Name=@"MARS\RichardGrimes")]
   static void OnlyRichard()
   {
      Console.WriteLine("OnlyRichard called");
   }
}

PrincipalPermission is the only non-CAS permission in the framework, so a stack walk is not performed. Instead, the system makes a check against the current thread principal. The first attribute has the name of a Windows group for the Role, BUILTIN\Administrators, so this means that any user that is a member of the Administrators group can call this method. In the second example the attribute has the name of a user, MARS\RichardGrimes, so only this specific user will be able to call this method, regardless of the groups he is a member of. You can replace this name with a string in the format <authority>\<user> where <authority> is the name of the authentication authority (which will be the domain controller if you are logged on using a domain account, or the local machine name if you are logged on with a local account).

Before you can call this code you need to set the security policy, and since a security exception will be thrown if the thread principal does not match that required by the principal permission it is prudent to enclose the call with a try/catch block.

Add the following into the Main method:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
try
{
   Console.WriteLine("called OnlyAdministrators");
   OnlyAdministrators();
}
catch(Exception)
{
   Console.WriteLine("OnlyAdministrators failed");
}
try
{
   Console.WriteLine("called OnlyRichard");
   OnlyRichard();
}
catch(Exception)
{
   Console.WriteLine("OnlyRichard failed");
}

Compile this:

csc roles.cs

If I run this on my machine I get the following results:

called OnlyAdministrators
OnlyAdministrators called
called OnlyRichard
OnlyRichard called

Try this out for different users and groups to convince yourself that the security exception will be thrown if the thread principal is not in the specified group, or is not the specified user.

The permission attribute works for custom principals too. Add the following method:

[PrincipalPermission(SecurityAction.Demand, Role="Managers")]
static void OnlyManagers()
{
   Console.WriteLine("OnlyManagers called");
}

This indicates that only principals who are a member of a custom role called Managers can call the method. To test it, add the following lines to Main and add a using statement for System.Threading:

GenericIdentity identity = new GenericIdentity("Richard");
Thread.CurrentPrincipal = new GenericPrincipal(identity, new string[]{"Managers"});
try
{
   Console.WriteLine("Called OnlyManagers");
   OnlyManagers();
}
catch(Exception)
{
   Console.WriteLine("OnlyManagers failed");
}

As you can see, I have created a new principal called Richard which is a member of the Managers role. Compile this code:

csc roles.cs

When you run this code you'll find that the OnlyManagers method will be called. Change the code so that the principal is a member of a different role, say, Customer, and confirm that the method will not be called.

When you compile code that has a permission attribute the compiler will create an instance of the attribute object and call the CreatePermission method This method will create an instance of the permission class associated with the attribute and initialize this object with the information provided to the attribute. So in the case of [PrincipalPermission] a PrincipalPermission object is created and its properties are initialized with the values provided by the attribute. The compiler then calls ToXml on the permission object to obtain the serialized form of this object. Finally, the XML is embedded in the assembly.

For example, run ILDASM on the executable that you have been testing and have a look at the OnlyAdministrators method. You should see something like this:

.method private hidebysig static void OnlyAdministrators() cil managed
{
   .permissionset noncasdemand = (/*omitted for brevity*/)
   .permissionset demand = (/*omitted for brevity*/)
    // Code size 11 (0xb)
    .maxstack 1
    IL_0000: ldstr "OnlyAdministrators called"
    IL_0005: call void [mscorlib]System.Console::WriteLine(string)
    IL_000a: ret
} // end of method App::OnlyAdministrators

The demand represents a CAS permission set that will be used for stack walks, the noncasdemand is a non-CAS permission. These items of metadata contain XML. The noncasdemand looks like this:

<PermissionSet class="System.Security.PermissionSet" version="1">
   <Permission class="System.Security.Permissions.PrincipalPermission,
         mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
      version="1">
      <Identity Authenticated="true", Role="BUILTIN\Administrators"/>
   </Permission>
</PermissionSet>
 
Note that I have split the class attribute over two lines (indicated by the symbol). As you can see, this XML describes a permission set that contains one permission, PrincipalPermission, and gives the values of two properties of this permission. For completeness, here is the XML for the demand metadata:

<PermissionSet class="System.Security.PermissionSet" version="1">
</PermissionSet>

This is an empty permission set.

When the OnlyAdministrators method is called, the runtime sees the noncasdemand and creates an instance of the specified class (PrincipalPermission), initializing the object's properties with the values given in the XML. Then the runtime calls Demand on the permission object.

.NET Version 3.0
This is not the mechanism that is used in version 3.0/2.0. The compiler will use the metadata of the attribute to get the type of the attribute and the parameters passed to it. This information is put in the demand metadata (not the noncasdemand) and at execution time the .NET runtime will create the attribute object with the supplied data and then call CreatePermission to get the permission object. ILDASM will give the following for the OnlyAdministrators method:

.method private hidebysig static void OnlyAdministrators() cil managed
{
.permissionset demand =
   {
      [mscorlib]System.Security.Permissions.PrincipalPermissionAttribute =
         {property string 'Role' = string('BUILTIN\\Administrators')}
   }

Reflector gives this for the implementation of PrincipalPermission.Demand:

public void Demand()
{
   IPrincipal principal = Thread.CurrentPrincipal;
   if (principal == null)
   {
      throw new SecurityException(
         Environment.GetResourceString("Security_PrincipalPermission"), base.GetType(), this.ToXml().ToString());
   }
   if (this.m_array != null)
   {
      int num1 = this.m_array.Length;
      bool flag = false;
      for (int num2 = 0; num2 < num1; num2++)
      {
         if (this.m_array[num2].m_authenticated)
         {
            IIdentity identity = principal.Identity;
            if ((identity.IsAuthenticated
                && (m_array[i].m_role == null || principal.IsInRole(m_array[i].m_role))
                && (m_array[i].m_id == null
                    || String.Compare(identity.Name, m_array[i].m_id, true, CultureInfo.InvariantCulture) == 0)))
            {
               flag = true;
               break;
            }
         }
         else
         {
            flag = true;
            break;
         }
      }
      if (!flag)
         throw new SecurityException(
            Environment.GetResourceString("Security_PrincipalPermission"), typeof(PrincipalPermission));
   }
}

In this code Demand first gets the thread principal, then it iterates through it's roles array, m_array. The roles array is an array of IDRole objects, each of these objects contains a field for the name of the user to be granted access, the name of the role to be granted access and a flag to indicate whether the principal must be authenticated. For each item in the role array Demand checks to see if the principal should be authenticated, if not then no check is necessary and the demand will succeed. If the principal should be authenticated then the identity of the principal is obtained and the check is performed. For the demand to succeed one of two conditions must be met:

  • the principal's identity is authenticated
  • the permission's role name is null or it has a name and the principal is in this role
  • the permission's ID is null or the ID has the same name as the principal's identity

Clearly the principal's IsInRole is an important part of the process. The method on WindowsPrincipal will obtain the names of the groups on the machine and compare the name you provide with those. An instance of GenericPrincipal is created with a string array of the role names, so the implementation of IPrincipal.IsInRole iterates over this array and compares each item with the role being checked.

.NET Version 3.0
The implementation of the demand in version 3.0/2.0 is slightly different. The first difference is that the first code is an Assertrt for the ControlPrincipal permission which means that partially trusted code calling the method with this demand does not have to have this permission. Prior to .NET 2.0 code downloaded from the intranet or the internet would not be able to use principel permissions because such code does not have the ControlPrincipal permission. The logic determining if the demand succeeds has also changed because the demand also performs a SID check between the principal and the roles that the permission will allow. The SID check takes precedence over a role name check.

8.3 Custom Role Checksks

In the previous example you should have become aware of one of the problems with .NET role based security. Imagine this scenario: you have a library which protects its classes with role based security. The library assumes that you will use Windows authentication, and so it uses [PrincipalPermission] attributes giving access to members of particular Windows groups. For example, a privileged method could give access only to members of the BUILTIN\Administrators group. The previous example had such a method called OnlyAdministrators. However, from the discussion in the last section you learned that the principal is used to make the role check, so if someone else provides the principal they can get it to agree to anything.

In my opinion this is a serious flaw in .NET role based security, and although it can be fixed, it is not immediately obvious that this flaw exists.

Edit the previous example so that you only have the Main and OnlyAdministrators methods and empty the Main method, also add the new using statement:

using System;
using System.Threading;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Security.Permissions;

class App
{
   static void Main()
   {
      // see later
   }
   [PrincipalPermission(SecurityAction.Demand, Role=@"BUILTIN\Administrators")]
   static void OnlyAdministrators()
   {
      Console.WriteLine("OnlyAdministrators called");
   }
}
Now add a new class for a custom principal:

class MyPrincipal : IPrincipal, IIdentity
{
   string name;
   string auth;
   bool isAuth;

   public MyPrincipal(string name, string auth, bool isAuth)
   {
      this.name = name;
      this.auth = auth;
      this.isAuth = isAuth;
   }
   public IIdentity Identity
   {
      get{return this;}
   }
   public bool IsInRole(string role)
   {
      return true;
   }
   public string AuthenticationType
   {
      get{return auth;}
   }
   public bool IsAuthenticated
   {
      get{return isAuth;}
   }
   public string Name
   {
      get{return name;}
   }
}

As you can see, this does little at all. For [PrincipalPermission] to work IPrincipal.IIdentity must return a valid reference and so I provide this by implementing this interface on the class with bare bones methods. The important thing is that the implementation of IsInRole always returns true. To test this, replace the Main method with this code:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Thread.CurrentPrincipal = new MyPrincipal("Richard", "NTLM", true);
try
{
   Console.WriteLine("called OnlyAdministrators");
   OnlyAdministrators();
}
catch(Exception)
{
   Console.WriteLine("failed");
}

Compile this:

csc roles.cs

Run this code and confirm that making the thread principal an instance of MyPrincipal will satisfy the demand on the OnlyAdministrators method. The principal is not created with the role that the method requires (indeed, there is no way to initialize the role with this principal type). Yet when this code is run the OnlyAdministrators method will be called. The problem is that the PrincipalPermission.Demand method calls IsInRole on an interface reference it does not care what class provides the implementation.

Thus, in the scenario I mentioned above, anyone can bypass role based security if they can change the thread principal. If you provide a utility library that will be loaded by third party processes then role based security is clearly at risk. However, even if you provide the process and the library, role based security can still be at risk if your code runs library code that is user-provided (for example, using some add-in mechanism). Any user-provided code that has full trust and runs in your application domain can change the thread principal.

The solution to this issue is to prevent any code from changing the thread principal, or to avoid using the rather weak implementation of PrincipalPermission.Demand. The former option will be covered in the next section. The latter option is quite simple to implement. Instead of using [PrincipalPermission] attribute to provide role checks, you can use your own role check code, for example:

[PrincipalPermission(SecurityAction.Demand, Role=@"BUILTIN\Administrators")]
static void OnlyAdministrators()
{
   if (IsInRole("Administrators"))
   {
      Console.WriteLine("OnlyAdministrators called");
   }
   else
   {
      Console.WriteLine("OnlyAdministrators failed");
   }
}

I have defined a function called IsInRole that will test to see if the windows principal is in the specified local group. Since this is an NT local group I have missed off the BUILTIN prefix. It would have been nice to be able to read the properties of the [PrincipalPermission] attribute, but as I have explained, this is not a custom attribute and hence it is not possible to read it with reflection.

.NET Version 3.0
Version 8 of the C# compiler will add the permission attribute using a similar mechanism as it does for custom attributes and hence you can read the data using reflection. In addition, you can access the SIDs of the groups that a user is a member of through WindowsIdentity and use NTAccount to get the group name. The OnlyAdministrators method will look like this:

[PrincipalPermission(SecurityAction.Demand, Role=@"BUILTIN\Administrators")]
static void OnlyAdministrators()
{
   Type type = typeof(App);
   System.Reflection.MethodInfo mi = type.GetMethod("OnlyAdministrators",
   System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
   object[] attributes = mi.GetCustomAttributes(typeof(PrincipalPermissionAttribute), false);
   if (attributes == null)
   {
      throw new System.Security.SecurityException("Principal permission denied");
   }

   bool found = false;
   foreach (object attribute in attributes)
   {
      PrincipalPermissionAttribute perm = attribute as PrincipalPermissionAttribute;
      if (perm.Role.IndexOf(@"BUILTIN\") == -1) continue;
      found = IsInRole(perm.Role.Substring(8));
      if (found) break;
   }
   if (!found)
   {
      throw new System.Security.SecurityException("Principal permission denied");
   }

   Console.WriteLine("OnlyAdministrators called");
}

static bool IsInRole(string group)
{
   WindowsIdentity identity = WindowsIdentity.GetCurrent();
   foreach (SecurityIdentifier sid in identity.Groups)
   {
      NTAccount account = (NTAccount)sid.Translate(typeof(NTAccount));
      if (account.Value == group) return true;
   }
   return false;
}

The following method performs the role check using a class called NTSecurity that we will define in a moment:

static bool IsInRole(string group)
{
   string[] members = NTSecurity.GetLocalGroupMembers(group);
   WindowsIdentity identity = WindowsIdentity.GetCurrent();
   foreach(string member in members)
   {
      if (member == identity.Name) return true;
   }
   return false;
}

It should be pointed out here that this method is defined in the application's class only as an example of the type of code that you could write. More robust code would put this method as a static method of a class in a library. You could then make sure that you assert for the ControlPrincipal permission and the UnmanagedCode permission to allow this code to be called by partially trusted code.

GetLocalGroupMembers method uses platform invoke to get the members of the specified local group and return them in a string array, the IsInRole simply iterates through this array and compares the items with the windows principal name. The NTSecurity class looks like this:

class NTSecurity
{
   const int MAX_PREFERRED_LENGTH = -1;

   [DllImport("netapi32", CharSet = CharSet.Unicode)]
   static extern int NetLocalGroupGetMembers(
      string serverName, string groupName,
      int level, out IntPtr buffer, int len,
      out int entriesRead, out int totalEntries, int handle);
   [DllImport("netapi32")]
   static extern int NetApiBufferFree(IntPtr buffer);

   [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
   struct LocalGroupMembersInfo3
   {
      public string lgrmi3_domainandname;
   }

   public static string[] GetLocalGroupMembers(string group)
   {
      IntPtr lgmi = IntPtr.Zero;
      int entriesRead = 0;
      int totalEntries = 0;
      int ret = NetLocalGroupGetMembers(null, group, 3,
         out lgmi, MAX_PREFERRED_LENGTH, out entriesRead, out totalEntries, 0);
      if (ret != 0)0)
      {
         return new string[0];
      }
      string[] users = new string[entriesRead];
      IntPtr iter = lgmi;
      for(int x = 0; x < entriesRead; ++x)
      {
         LocalGroupMembersInfo3 members =
            (LocalGroupMembersInfo3)Marshal.PtrToStructure(iter, typeof(LocalGroupMembersInfo3));
         users[x] = members.lgrmi3_domainandname;
         iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LocalGroupMembersInfo3)));
      }
      NetApiBufferFree(lgmi);
      return users;s;
   }
}

The full code can be found here.

The NetLocalGroupGetMembers function returns the names of the users in the specified local group. I will leave it up to the reader to read MSDN library to get a full description of this function, but in brief I will mention the parameters I have used. The first parameter is null to indicate that I want to use a group on the local machine and the name of that group is given as the second parameter. I want to get the names of the users in this group, so I request that it returns data level 3, that is, data in LOCAL_GROUP_MEMBERS_INFO_3 structures (I have created a nested managed version of this structure called LocalGroupMembersInfo3). The fifth parameter is MAX_PREFERRED_LENGTH to indicate that the system should allocate the buffer for me and return it in the fourth parameter. The number of LOCAL_GROUP_MEMBERS_INFO_3_3 items in the buffer is returned through the sixth parameter. Calling this method this way means that all members are returned in one go, so the final parameter is zero to indicate that I don't want to call this function several times to get the data.

Since the system allocates the buffer I need to tell the system to de-allocate the memory after I have finished using it. This is the reason for the call to NetApiBufferFree at the end of the method. If the call to NetLocalGroupGetMembers is successful then I iterate through the buffer and obtain the name of the group member.

Compile this code and run it. If your user account is a member of the e Administrators group you will find that the method will succeed. Change this code to a built-in group that you are not a member of (for example, Power Users, Backup Operators or Guests) and confirm that you will be denied access.

8.4 Principal Control

The previous section showed how to obtain the Windows principal, and how to obtain the members in a Windows local group to see if the Windows principal is a member of the group. Third party code is unable to change the Windows principal, because to do this would require the code to impersonate another user and this implies that the third party knows the name and password of this user; also the code must have the SE_TCB_NAME NT privilege, which is unlikely. However, the previous code shows that if you have  plug-in architecture then plug-in code can change the thread principal to a custom principal class that behaves as if your code has a more privileged account than it does have. Clearly you would want to prevent this. The previous section showed that role security attributes cannot detect this situation, one solution is to write your own principal attribute, which is straight forward given the explanation given above.

There is another option. If you read the MSDN documentation for Thread.CurrentPrincipal you'll see that to change the CurrentPrincipal your code must have the SecurityPermission of ControlPrincipal. This is a code access security permission, which will be assigned to all fully trusted code (the partially trusted case will be discussed later). You can leverage this to prevent third party code changing the thread principal by refusing the assembly this permission, so that a security exception will be thrown if a third-party assembly triggers a tack walk for this permission when it attempts to change the thread principal.

The simplest way to do this would appear to be to add a [SecurityPermission] on the assembly denying ControlPrincipal, however this permission is required for all access to thread principal and so the call to SetPrincipalPolicy will throw an exception if you refuse this permission. You need a finer grain approach and so a better approach is to imperative security in the entry point of the process after you have called SetPrincipalPolicy. Clean up the example so that it looks like this (remove the call to IsInRole, remove this method and the NTSecurity class):

using System;m;
using System.Security.Principal;
using System.Security.Permissions;

class App
{
   static void Main()
   {
      AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
      try
      {
         Console.WriteLine("called OnlyAdministrators");
         OnlyAdministrators();
      }
      catch(Exception)
      {
         Console.WriteLine("failed");
      }
   }
   [PrincipalPermission(SecurityAction.Demand, Role=@"BUILTIN\Administrators")]
   static void OnlyAdministrators()
   {
      Console.WriteLine("OnlyAdministrators called");
   }
}

To simulate the effect of running plug-in code create a library called plug-in.cs and add the following code:

using System;
using System.Security.Principal;
using System.Threading;

public class Plugin
{
   public void Initialize()
   {
   }
}

Now move the MyPrincipal class from roles.cs to plug-in.cs and move the line that initializes the principal from the Main function (in roles.cs) to Initialize (in plug-in.cs). Now place a call to create an instance of the Plugin class and call its Initialize method.

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Plugin plugin = new Plugin();
plugin.Initialize();

The code that changes the thread principal has been moved to the plug-in code, which is called in the Main method. Compile the code:

csc /t:library plug-in.cs
csc roles.cs /r:plug-in.dll

Run this code and confirm that the plug-in code has changed the thread principal in such a way that allows privileged code to be called. Now

Change the process code so that it denies access to the thread principal:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
SecurityPermission perm = new SecurityPermission(SecurityPermissionFlag.ControlPrincipal);
perm.Deny();

Plugin plugin = new Plugin();

That was easy! Now run the code and check that a security exception will be thrown when the user tries to change the thread principal.

However, it is not as simple as that. Comment out the line to change the thread principal and run te example again. You'll see that the call to OnlyAdministrators fails. The reason for this was given earlier. The thread principal property uses a cached value and if this value is null then it will initialise the principal to the Windows principal the first time that the thread principal is accessed. However, in this example the first time that the thread principal is accessed is during the call to PrincipalPermission.Demand which is after I have denied the assembly access to the thread principal. This means that reading the thread principal will cause a CAS security exception and hence the demand will always fail.

To get round this issue you need to make sure that the thread principal is set before the permission deny is made. The simplest way to do this is to call SetThreadPrincipal on the application domain:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
AppDomain.CurrentDomain.SetThreadPrincipal(new WindowsPrincipal(WindowsIdentity.GetCurrent()));
SecurityPermission perm = new SecurityPermission(SecurityPermissionFlag.ControlPrincipal);

Now you'll find that the code will fail if the plug-in tries to change the thread principal, but the code will work otherwise.

This is the case with assemblies that have full trust because such assemblies have the ControlPrincipal permission and so are able to change the principal policy. Code that has the Internet or LocalIntranet permissions do not have the ControlPrincipal permission, however, this should not be an issue because you will not want partially trusted code to change the thread principal.

8.5 Enterprise Services Role Based Security

One of the big innovations when MTS was released was its use of role based security. COM+, released first in Windows 2000 and then updated for Windows XP and Windows 2003 Server, inherited this form of security. I explained in my MTS book that there was nothing new in role based security, and that you could implement much of it in C++. The big difference was that the role check was applied through interception before a method was called, but again, this can be simulated in C++ if a disciplined programmer called an appropriate access check method as the first line of each component method. I explained in my book that the main reason for role based security in MTS (indeed, the main reason for most of MTS's facilities) was to give security to Visual Basic developers. Prior to MTS Visual Basic developers found it difficult to do security programming (and consequently they ignored it) and in some cases the Visual Basic runtime prevented some types of security programming (in particular, the facilities offered by CoInitializeSecurity).

.NET provides access to COM+ through the Enterprise Services classes. Note that these are wrapper classes that access the COM+ API through COM interop. COM+ (and in a more limited way, MTS) was revolutionary because it allowed you to configure classes. This means that you used the COM+ component explorer to add attributes to COM classes and these attributes are stored in the COM+ catalog. When a client program creates an instance of such a configured class the COM+ runtime reads the catalog to get the class attributes to determine the extra component services that it will apply to this instance. The one weakness of COM+ is that these COM+ attributes (metadata, if you like) resides in the COM+ catalog which is separate to the implementation of the class. Logically the attributes should be part of the class.

.NET Enterprise Services allows you to add COM+ attributes to any .NET class that derives from ServicedComponent. However, don't be mislead by the terminology or by the similarity with other attributes in .NET. The Enterprise Services attributes are not recognised by the .NET runtime, nor are they recognised by the COM+ runtime. They are not even accessed at runtime. Enterprise Services attributes are used to populate the COM+ catalog, and it is the values in the COM+ catalog that are used at runtime to determine the services that will be applied to a component. (Contrast this with .NET component services, like thread synchronization, that are applied at runtime with .NET contexts.)

The metadata in Enterprise Services attributes get written into the COM+ catalog at one of two times. First, an assembly can be installed into COM+ by calling the regsvcs command line utility. This tool uses reflection to scan through the assembly for Enterprise Services attributes and then add the appropriate information into the catalog. Some of these attributes can be applied to the assembly to define the COM+ application that will contain the components. If your user is a member of the local  Administrators group then you can use the second mechanism to add attributes to the catalog: dynamic registration. In this case, the first time that your application uses a serviced component the runtime will read its Enterprise Services attributes and add them to the catalog. It is usually better to use the manual method with regsvcs.

To test this out you need to create a library assembly. This assembly will have the components that will run in your Enterprise Services application. You also need to create a process assembly that will call the components in the application. The library is simple (lib.cs):

using System;
using System.EnterpriseServices;
using System.Reflection;

[assembly: ApplicationName("Bank")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

public interface IAccount
{
   float Read();
   void Withdraw(float x);
   void Deposit(float x);
}

public class Account : ServicedComponent, IAccount
{
   float account = 1000;
   public float Read()
   {
      return account;
   }
   public void Withdraw(float x)
   {
      account -= x;
   }
   public void Deposit(float x)
   {
      account += x;
   }
}

The assembly must have a strong name, and a text name. You should also provide an access control attribute so that regsvcs will determine whether access control security will be used. At this point we will switch off access control, but later in this section we will switch it on. Finally, the activation attribute determines whether the components will run in the same process as the client, or whether a separate server process will be used.

The component is derived from ServicedComponent which indicates that it can be added to the COM+ component catalog and that the COM+ runtime can add component services at run time. Note that I have declared an interface and the component derives from this interface. The reason is that COM+ is an interface based technology and so you should use interfaces on your components. This will become more apparent when you add roles to this application.

The application that uses this library is straightforward. It is a Windows Forms application with three buttons to allow you to access the methods of the Account class, a text box to allow you to provide values for the account, and a label to display results. Here is the code:

using System;
using System.Windows.Forms;
using System.Drawing;

class MainForm : Form
{
   static void Main()
   {
      Application.Run(new MainForm());
   }

   Button btnDeposit;
   Button btnWithdraw;
   Button btnRead;
   TextBox txtAmount;
   Label lblAmount;
   Label lblResult;

   IAccount account;

   public MainForm()
   {
      this.ClientSize = new Size(300, 130);
      this.Text = "Bank Account";
      btnDeposit = new Button();
      btnDeposit.Text = "Deposit";
      btnDeposit.Location = new Point(10, 10);
      btnDeposit.Click += new EventHandler(btnDeposit_Click);
      btnWithdraw = new Button();
      btnWithdraw.Text = "Withdraw";
      btnWithdraw.Location = new Point(110, 10);
      btnWithdraw.Click += new EventHandler(btnWithdraw_Click);
      btnRead = new Button();
      btnRead.Text = "Read";
      btnRead.Location = new Point(210, 10);
      btnRead.Click += new EventHandler(btnRead_Click);
      lblAmount = new Label();
      lblAmount.Text = "Amount:";
      lblAmount.Location = new Point(10, 50);
      txtAmount = new TextBox();
      txtAmount.Location = new Point(110, 50);
      txtAmount.Width = 170;
      lblResult = new Label();
      lblResult.BorderStyle = BorderStyle.FixedSingle;
      lblResult.Location = new Point(10, 90);
      lblResult.Width = 270;
      this.Controls.AddRange(
      new Control[]{btnDeposit, btnWithdraw, btnRead,
                    txtAmount, lblAmount, lblResult});

      account = new Account();
   }
   void btnDeposit_Click(object sender, EventArgs args)
   {
      account.Deposit(Single.Parse(txtAmount.Text));
   }
   void btnWithdraw_Click(object sender, EventArgs args)
   {
      account.Withdraw(Single.Parse(txtAmount.Text));
   }
   void btnRead_Click(object sender, EventArgs args)
   {
      lblResult.Text = account.Read().ToString();
   }
}

Most of this code is used to set up the user interface. Notice that this code does not contain any Enterprise Services code. It treats the component as a .NET component. The reason is that COM+ will apply its services when your code actually makes a call to the component. This is interception, however, it is COM+ interception and not .NET interception. The user interface looks like this:

Here is a makefile to build this assembly, since there are so many steps, using a makefile makes life easier:

all : app.exe

clean :
   -@regsvcs /u lib.dll
   -@gacutil /u lib
   -@del lib.dll
   -@del lib.tlb
   -@del app.exe

app.exe : app.cs lib.dll
   csc /t:winexe app.cs /r:lib.dll

lib.dll : lib.cs
   -@regsvcs /u lib.dll
   csc /t:library lib.cs
   gacutil /i lib.dll
   regsvcs lib.dll

The library is compiled like any other .NET library assembly, however, once it is built it is added to the GAC and then added to the COM+ component catalog. Adding it to the GAC is not strictly necessary in this example, but is a good idea so that the library is in a known location. The regsvcs utility will create a type library so that unmanaged clients can use the component. Before the library is compiled it is removed from the catalog so that the catalog update will add the assembly's values to a fresh catalog. I prefix the command with -@ so that if the uninstall fails (as will be the case the first time the library is created) the following commands will still be executed. The clean target will remove the application from the COM+ component catalog, remove it from the GAC and then clean up all the files it created including the type library.

Make the library and process with:

nmake

Now open the Component Services applet from the Administrative Tools control panel folder (or launch it by typing \WINDOWS\system32\com\comexp.msc in the Run dialog). This will launch Microsoft Management Console with the COM+ Component Services Explorer. This tool has its quirks. If you open the Component Services node by clicking on the + symbol, you'll find that the node is empty. This is not right. Instead you have to select the node, and then open the node by clicking on the + symbol. After the first node has been opened MMC will populate the tree correctly and you'll be able to open child nodes normally.

Open Computers, then My Computer and then COM+ Applications. You'll see that there will be an entry for the Bank application.

Notice that under the Bank node there are three nodes, Components are COM+ configured components, Legacy Components are COM components that are not configured and Roles contains the roles that the application will use.

Select the COM+ Applications node. In the right hand panel you'll see a collection of icons, one for each application. The icon with the cogs indicates a system application implemented in a service, the application called System Application will be running and you will be able to see this because the cogs will be turning. The applications with an icon that shows a ball outside of a box (for example, .NET Utilities in the picture above) are library applications. This means that the COM+ application will be loaded into the process space of the calling application. Finally, the icon that has a ball in a box (for example, our Bank application) is a server application which runs in a process (dllhost.exe) separate to the application that uses its objects. When an application runs, the ball in the icon will appear to rotate. To stop the COM+ application you need to shutdown the processes that use the COM+ application. By default the COM+ application will run for 3 minutes after the consumer has closed, but you can close the COM+ application through the Shutdown item on the application's context menu.

Now run the application, notice that the icon for the Bank application will start to animate. First click on the Read button and you'll see that you have 1000. Type a value in the Amount box and click on Deposit or Withdraw and then click on Read to see that the account amount changes accordingly. Close the application, notice that the icon continues to be animated, showing that the application is still running. Right click on the icon and select Shutdown to stop the application.

Now for the rules that we want this application to follow:

  • Anyone can deposit an amount in the account (and I would like it if someone would!)
  • Only a customer can withdraw from an account
  • A customer or a teller can read the account
  • A bank manager has total access to any account and can deposit, withdraw or read an account

The first thing to do is to enable application access control:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(true)]

When you do this it means that the identity of the caller must be a member of at least one of the roles defined in the application before the COM+ application will be launched. Try this. Compile the code and run the application. You'll find that an UnauthotizedAccessException is thrown, but it is not be clear what the exception is. To see this exception add a thread exception handler to the process:

using System.Drawing;
using System.Threading;

class using Form
{
   static void Main()
   {
      Application.ThreadException += new ThreadExceptionEventHandler(ExceptionHandler);
      Application.Run(new MainForm());
   }

   static void ExceptionHandler(object sender, ThreadExceptionEventArgs args)
   {
      MessageBox.Show(args.Exception.ToString());
      Application.Exit();
   }

This sets up a handler that will be called if an exception is thrown on the main thread. However, note that the window must be created before this handler will be called, so rather than creating the Account object in the constructor (which will be called before the window is created) you should create a Load handler to do this. So remove the line from the constructor that creates the Account object and add the following code:

void this_Load(object sender, EventArgs args)
{
   account = new Account();
}

public MainForm()
{
   this.ClientSize = new Size(300, 130);
   this.Text = "Bank Account";
   this.Load += new EventHandler(this_Load);

Compile and run the application. You'll see that the problem before was that an UnauthorizedAccessException is thrown. As I mentioned above, you need to add at least one role to the application and make sure that your account is a member of that role. There are two ways to do this, we will first use the COM+ Component Services Explorer and then you'll use code.

If the explorer is already open, right click on COM+ Applications and select Refresh so that new values are read from the catalog. Then open Bank and select Roles. Right click on this node and select New and then Role. Give the role the name Everyone, click on OK and then open this node and right click on Users and then select New and then User. In the box Enter the object name to select type Everyone and then click on Check Names. The dialog will check that the group exists and if the check succeeds the entry will be underlined. Finally click on OK. You'll find that the Everyone role will contain the Everyone NT group.

Now run the application and you'll find that the application will now run without an exception. What you have done is added a new role that is assigned to everyone on the machine. The COM+ application runs because there is a role for everyone who will run the application.

We will use four roles, Everyone, Teller, Customer and Manager The COM+ application needs these four roles so it makes sense that these are added through the assembly, so add the following lines to the library code (lib.cs):

[assembly: AssemblyKeyFile("key.snk")]
[assembly: AssemblyVersion("1.0.0.0")]

[assembly: SecurityRole("Everyone", SetEveryoneAccess=true)]
[assembly: SecurityRole("Teller")]
[assembly: SecurityRole("Customer")]
[assembly: SecurityRole("Manager")]

Note that you do not add any accounts to the roles because as a developer you do not know what accounts will be available on the local machine or domain. The only exception to this is the Everyone group which you give access to everyone using SetEveryoneAccess. Compile this code using nmake and then refresh COM+ Applications in the explorer and verify that the roles have been added to the application.

Finally, we want to add role based security to the code. Again, you can do this through code, or through the Component Services explorer. We will use the explorer first. Open the Components node, then Account, then Interfaces and finally open IAccount. You'll see the three interface methods. Now, one by one, look at the properties (through the right click context menu) for each method, and for the IAccount interface and the Account component. Each property dialog will have a Security tab, which will look something like this:

As you can see, this lists the roles that were added through the assembly. This dialog is for a method (Withdraw) and you use it to indicate the roles that will have access to the method. The dialog for the interface is similar and indicates that all methods on the interface can be accessed by accounts in the selected roles. The property page for the component is similar but does not have the top box Roles inherited by selected items. The accounts in the selected roles on the component's page will be able to access all interfaces and all methods on these interfaces. Clearly if you select a role on the component's page those roles should be selected on the interface page, and any roles selected on the interface page should be selected on the method page. You can add extra roles, but you cannot remove them. In addition the component's page has a check box marked Enforce component level access checks. If you check this box (as you should) then the runtime will perform a role check - that is, make sure that the user is in at least one of the component's roles - before accessing the component.

Use the property boxes to change the roles for each of the methods. Accounts in the Manager role will be allowed to access all the methods on the component, so use the component's (Account not the class interface _Account) security page to check the Manager item. Ensure that the Enforce component level access checks check box is checked. Next, open the property page for the Read method and check the Teller item and Customer item, the Manager item should already be inherited. Open the property page for Withdraw and check the Customer item. Finally open the property page for the Deposit method and check Everyone.

Now provide values for the roles. Open the Roles node, then Teller, right click on Users and select New then User. This role should contain the Administrators group. Since this is a local group you first have to indicate that you want the dialog to check all machine groups, so on implementation provided with XP SP2 click on the Object Types button and make sure that the Group item is selected, then click OK. Next, type Administrators in the dialog and click on the Check Names button. This should verify the name of the local machine's Administrators group. (Confusingly, it will give the name of the group in the form <authority>\Administrators where <authority> for the local machine will be the machine name, however, when you add it to the role the explorer will list the group as BUILTIN\Administrators.) The Managers role should also contain the Administrators group, so repeat the previous steps. Finally, add your own account to the Customer role.

Now run the application and verify that you can call each of the three methods. To test that

To apply these roles declaratively you use the [SecurityRole] attribute, but this time, you add it to a method, interface or class. Change the component class, adding the following attributes:

[ComponentAccessControl(true)]
[SecurityRole("Manager")]
public class Account : ServicedComponent, IAccount
{
   float account = 1000;

   [SecurityRole("Customer")]
   [SecurityRole("Teller")]
   public float Read()
   {
      return account;
   }
   [SecurityRole("Customer")]
   public void Withdraw(float x)
   {
      account -= x;
   }
   [SecurityRole("Everyone")]
   public void Deposit(float x)
   {
      account += x;
   }
}

Notice that I have used [ComponentAccessControl] to make sure that component level access checks are performed. Delete the Bank application in the explorer, so that you can see that these attributes are used to add the roles to the component. Next make the project with nmake. Now select COM+ Applications and from the context menu select Refresh. Look at the security tab for the component and each of its methods and confirm that the appropriate roles have access. Before you can use the application you must give accounts to the roles.

This section has only touched upon the subject of COM+ security. However, as you can see there are some significant differences between how it works and how role based security works in .NET. You have no choice at all about how principals are implemented, they have to be Windows accounts. This makes it far harder for rogue code to spoof a principal because as I have mentioned already, the code will automatically get an acce3ss token from the system and the only way that code can spoof another account is if it has the privileges required to impersonate users. The downside is that you have the overhead of the COM+ runtime (even if your COM+ application runs as a library application, that is, in-process), but this is acceptable considering the extra piece of mind it gives you.

To clean up the application remove the application from the catalog and from the GAC by calling:

nmake clean


I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Nine

This page is (c) 2007 Richard Grimes, all rights reserved