how to write tests that impersonates different users?
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);
}
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.
You can also set the current principal directly if that's sufficient for your use case:
System.Threading.Thread.CurrentPrincipal
= new WindowsPrincipal(new WindowsIdentity("testuser@contoso.com"));
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).
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.
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.
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("testuser@contoso.com"));
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 :)
精彩评论