how to write tests that impersonates different users?
Asked Answered
E

5

6

My Winforms app set permissions based on the group membership found in the current process.

I just made a unit test in MSTEST.

I'd like to run it as other users so I can verify the expected behavior.

Here's what I'm kind of shooting for:

    [TestMethod]
    public void SecuritySummaryTest1()
    {
        Impersonate(@"SomeDomain\AdminUser", password);
        var target = new DirectAgentsSecurityManager();
        string actual = target.SecuritySummary;
        Assert.AreEqual(
            @"Default=[no]AccountManagement=[no]MediaBuying=[no]AdSales=[no]Accounting=[no]Admin=[YES]", actual);
    }
    [TestMethod]
    public void SecuritySummaryTest2()
    {
        Impersonate(@"SomeDomain\AccountantUser", password);
        var target = new DirectAgentsSecurityManager();
        string actual = target.SecuritySummary;
        Assert.AreEqual(
            @"Default=[no]AccountManagement=[YES]MediaBuying=[no]AdSales=[no]Accounting=[YES]Admin=[NO]", actual);
    }
Eyler answered 22/3, 2011 at 20:19 Comment(3)
even though I'm testing exactly one class's property? Is it the fact that there's a dependency to the o/s security subsys that overrides?Eyler
@LasseV.Karlsen. Can you explain why impersonating a user doesn't make it a unit test? If I have a method that should fail when the user doesn't have permissions, I'd like to make a unit test for that scenario to achieve code coverage.Kellby
You're relying on the surrounding environment, like a user being present on your domain/server, the user is still active, the password is still correct etc. Instead, mock out the part that retrieves permissions so that the code under test can be tested without having to impersonate the user. Any test that depends on the environment (ie. things outside your code) being set up just right are integreation tests, not unit tests.Agogue
F
11
public class UserCredentials
{
    private readonly string _domain;
    private readonly string _password;
    private readonly string _username;

    public UserCredentials(string domain, string username, string password)
    {
        _domain = domain;
        _username = username;
        _password = password;
    }

    public string Domain { get { return _domain; } }
    public string Username { get { return _username; } }
    public string Password { get { return _password; } }
}
public class UserImpersonation : IDisposable
{
    private readonly IntPtr _dupeTokenHandle = new IntPtr(0);
    private readonly IntPtr _tokenHandle = new IntPtr(0);
    private WindowsImpersonationContext _impersonatedUser;

    public UserImpersonation(UserCredentials credentials)
    {
        const int logon32ProviderDefault = 0;
        const int logon32LogonInteractive = 2;
        const int securityImpersonation = 2;

        _tokenHandle = IntPtr.Zero;
        _dupeTokenHandle = IntPtr.Zero;

        if (!Advapi32.LogonUser(credentials.Username, credentials.Domain, credentials.Password,
                                logon32LogonInteractive, logon32ProviderDefault, out _tokenHandle))
        {
            var win32ErrorNumber = Marshal.GetLastWin32Error();

            // REVIEW: maybe ImpersonationException should inherit from win32exception
            throw new ImpersonationException(win32ErrorNumber, new Win32Exception(win32ErrorNumber).Message,
                                             credentials.Username, credentials.Domain);
        }

        if (!Advapi32.DuplicateToken(_tokenHandle, securityImpersonation, out _dupeTokenHandle))
        {
            var win32ErrorNumber = Marshal.GetLastWin32Error();

            Kernel32.CloseHandle(_tokenHandle);
            throw new ImpersonationException(win32ErrorNumber, "Unable to duplicate token!", credentials.Username,
                                             credentials.Domain);
        }

        var newId = new WindowsIdentity(_dupeTokenHandle);
        _impersonatedUser = newId.Impersonate();
    }

    public void Dispose()
    {
        if (_impersonatedUser != null)
        {
            _impersonatedUser.Undo();
            _impersonatedUser = null;

            if (_tokenHandle != IntPtr.Zero)
                Kernel32.CloseHandle(_tokenHandle);

            if (_dupeTokenHandle != IntPtr.Zero)
                Kernel32.CloseHandle(_dupeTokenHandle);
        }
    }
}

internal static class Advapi32
{
    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool DuplicateToken(IntPtr ExistingTokenHandle, int SECURITY_IMPERSONATION_LEVEL,
                                             out IntPtr DuplicateTokenHandle);

    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword,
                                        int dwLogonType, int dwLogonProvider, out IntPtr phToken);
}

internal static class Kernel32
{
    [DllImport("kernel32.dll", SetLastError = true)]
    [return : MarshalAs(UnmanagedType.Bool)]
    public static extern bool CloseHandle(IntPtr hObject);
}

I didn't include the implementation of ImpersonationException but it's not important. It doesn't do anything special.

Frilling answered 22/3, 2011 at 20:24 Comment(0)
E
5

You can also set the current principal directly if that's sufficient for your use case:

System.Threading.Thread.CurrentPrincipal 
    = new WindowsPrincipal(new WindowsIdentity("[email protected]"));

The principal is restored after each test method according to this connect page. Note that this method won't work if used with web service clients that check the principal (for this use case, Jim Bolla's solution works just fine).

Encratia answered 30/3, 2011 at 9:4 Comment(0)
Z
2

You should use Mock objects to simulate dependent objects in different states. See moq for an example of a mocking framework:

You would need to abstract out the bit that provides the current user behind an interface. And pass in a mock of that interface to the class under test.

Zeebrugge answered 22/3, 2011 at 20:27 Comment(0)
I
2

Another thing to add to Markus's solution, you may also need to set HttpContext.Current.User to the Thread.CurrentPrincipal you are creating/impersonating for certain calls to the RoleManager (eg: Roles.GetRolesForUser(Identity.Name) ) If you use the parameterless version of the method this is not needed but I have an authorization infrastructure in place that requires a username to be passed.

Calling that method signature with an impersonated Thread.CurrentPrincipal will fail with "Method is only supported if the user name parameter matches the user name in the current Windows Identity". As the message suggests, there is an internal check in the WindowsTokenRoleProvider code against "HttpContext.Current.Identity.Name". The method fails if they don't match.

Here's sample code for an ApiController demonstrating authorization of an Action. I use impersonation for unit and integration testing so I can QA under different AD Roles to ensure security is working before deployment.

using System.Web

List<string> WhoIsAuthorized = new List<string>() {"ADGroup", "AdUser", "etc"};

public class MyController : ApiController {
    public MyController() {
     #if TEST 
        var myPrincipal = new WindowsPrincipal(new WindowsIdentity("[email protected]"));
        System.Threading.Thread.CurrentPrincipal = myPrincipal;
        HttpContext.Current.User = myPrincipal;
     #endif
    }
    public HttpResponseMessage MyAction() {
       var userRoles = Roles.GetRolesForUser(User.Identity.Name);
       bool isAuthorized = userRoles.Any(role => WhoIsAuthorized.Contains(role));
    }
}

Hope this helps someone else :)

Inconvenience answered 14/5, 2014 at 14:30 Comment(0)
I
2

Use SimpleImpersonation.

Run Install-Package SimpleImpersonation to install the nuget package.

Then

var credentials = new UserCredentials(domain, username, password);
Impersonation.RunAsUser(credentials, LogonType.NewCredentials, () =>
{
    // Body of the unit test case. 
}); 

This is the most simple and elegant solution.

Idealistic answered 21/9, 2018 at 21:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.