Unit Test and Impersonation

Usually as a developer I am logged with a user with more rights than a usual user. Even if I am not using the Admin account often I have to create one or more user with associated groups to simulate my target environment and log with those user and test my application. This is time consuming for me and i want to be sure I can retest those cases as often and as fast as I want. The idea may seem strange as those tests looks more like integration tests, but i don’t want to deploy my application, test manually with different accounts the expected behavior and play ping pong between integration and dev environment to correct the issues.

So I need to test the behavior of my code according to the rights of the user running it (this is all about Authorization). This can occur when access to classes or methods are limited to some users, when external resources (DB, Files…) with specific rights are involved.

To industrialize this in Unit Tests I am using an Helper class with Unmanaged code.

Create an Unmanaged project with the following code :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    using System;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    
    [assembly: SecurityPermissionAttribute(SecurityAction.RequestMinimum, UnmanagedCode = true)]
    [assembly: PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
    namespace Unmanaged.Helpers.SafeNativeMethods

    {
        public static class Impersonalisator
        {
            const Int32 LOGON32_PROVIDER_DEFAULT = 0;
            const Int32 LOGON32_LOGON_INTERACTIVE = 2;

            [DllImport("advapi32.dll", SetLastError = true)]
            private static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
                int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

            [DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
            private unsafe static extern int FormatMessage(int dwFlags, ref IntPtr lpSource,
                int dwMessageId, int dwLanguageId, ref String lpBuffer, int nSize, IntPtr* Arguments);

            [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
            public extern static bool CloseHandle(IntPtr handle);

            [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
                int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

            public static bool LogonUser(String userName, String domain, String password, ref IntPtr identityToken)
            {
                return LogonUser(userName, domain, password, 2, 0, ref identityToken);
            }
        }
    }

This helper class will manage the impersonation.

In my Test Classes, I create a new test project referencing the helper class we created above and I am writing my tests as follow:

Negative Test : Trying to log with an inexistent user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    [TestMethod]
    public void TestAsUnknownUser()
    {
        // Create the security Context
        IntPtr myToken = IntPtr.Zero;
        String userName = "FakeUser";
        WindowsIdentity newId;
        WindowsImpersonationContext impersonatedUser;
        Assert.IsFalse(Impersonalisator.LogonUser(userName, "VILFHA", "FakeUserPwd", ref myToken));

        // Reset the Security Context
        impersonatedUser.Undo();
        
        // Check the identity.
        Console.WriteLine("After Undo: " + WindowsIdentity.GetCurrent().Name);

        // Free the tokens.
        if (myToken != IntPtr.Zero)

        Impersonalisator.CloseHandle(myToken);
    }

Positive and Negative Test. According to how the code is implemented the content of the test will change. The aim is to use the different user privileges on all the securable. The content of the Assert statements will vary and or the [ExpectedException(typeof(namespace.NamedException))] attribute can also be used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
   [TestMethod]
   public void TestKnownUser()
   {
       // Create the security Context
       IntPtr myToken = IntPtr.Zero;
       String userName = "UserRole1";
       WindowsIdentity newId;
       WindowsImpersonationContext impersonatedUser;

       Boolean loginSuccessfull = Impersonalisator.LogonUser(userName, "VILFHA", "UserRole1Pwd", ref myToken);
       Assert.IsTrue(loginSuccessfull);

       if (loginSuccessfull)
       {
           newId = new WindowsIdentity(myToken);
           impersonatedUser = newId.Impersonate();
           Console.WriteLine("Impersonalisation worked. Current user : {0}", WindowsIdentity.GetCurrent().Name);
       }
       else
       {
           // To ensure we keep a trace if something wrong happen when impersonating
           int ret = Marshal.GetLastWin32Error();
           Console.WriteLine("LogonUser failed with error code : {0}", ret);
           throw new System.ComponentModel.Win32Exception(ret);
       }

       // Check if the impersonation ran fine (This double check the impersonation)
       Assert.IsTrue(WindowsIdentity.GetCurrent().Name.Contains(userName), 
           String.Format("Current identity is {0}. Impersonation was expecting {1}", WindowsIdentity.GetCurrent().Name, userName));

       // Perform Test with the current Identity
       //TODO: Add Code to test access

       // Reset the Security Context
       impersonatedUser.Undo();

       // Check the identity.
       Console.WriteLine("After Undo: " + WindowsIdentity.GetCurrent().Name);

       // Free the tokens.
       if (myToken != IntPtr.Zero)
       Impersonalisator.CloseHandle(myToken);

   }

Possible Optimizations As I used those tests, I realized a way to implement tests more easily is to create one Test Class per user and create the impersonation context in the ClassInitialize() methods. It makes the code much more readable.

Before using this solution I have been looking for an Attribute to give the credentials i wanted to use for the test.
I was thinking it would be nice to add that feature, but at the moment I m mitigated. I don’t think Unit Test are a good place to test security yet in my case this was the best place to do it.

Sources: [MSDN : System.Security.SecurityContext] ( http://msdn.microsoft.com/en-us/library/system.security.securitycontext.aspx )