8

We use NTLM auth in our application to determine whether a user can perform certain operations. We use the IPrincipal of their current Windows login (in WinForms applications), calling IsInRole to check for specific group memberships.

To check that a user is a local administrator on the machine, we use:

AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
...
bool allowed = Thread.CurrentPrincipal.IsInRole(@"Builtin\Administrators")

This works if the current user is the Administrator user, or is another user that is a member of the Builtin\Administrators group.

In our testing on Windows 7, we have found that this no longer works as expected. The Administrator user still works fine, but any other user that is a member of the Builtin\Administrators group returns false for the IsInRole call.

What could be causing this difference? I have a gut feeling that a default setting has changed somewhere (possible in gpedit), but cannot find anything that looks like the culprit.

adrianbanks
  • 81,306
  • 22
  • 176
  • 206
  • After asking this question, I had a play around with some code that manually checked the group memberships and replicated the behaviour on a Vista machine. That then led me onto the idea that UAC was the problem - most of the machines running Vista here have UAC turned off. The application checks for membership of groups defined from another source, so it can vary as to whether the application needs to be run as an administrator - a manifest file is too heavy-handed. I think changing the default groups in our application is probably the easiest solution. – adrianbanks Apr 21 '10 at 11:25

5 Answers5

9

The problem is that Windows security (aka "UAC") is getting in your way. There's special handling of administrator roles and your user will not actually have these roles until he is elevated. Admin roles are "ghosted" in a sense: present but unavailable for permission checks or even to (easily) test for presence. See the note at: http://msdn.microsoft.com/en-us/library/46ks97y7.aspx

Here's a series that talks about the issue, with example code that does the necessary workarounds:

I solved a similar problem in an ASP.NET app by building my own UAC prompt and using the name & password to call the Win32 Logon API. You might be lucky enough to be in a .NET desktop app, in which case you can use regular elevation requests.

Here's some C# code to check admin permissions without elevating.

    public const UInt32 TOKEN_DUPLICATE = 0x0002;
    public const UInt32 TOKEN_IMPERSONATE = 0x0004;
    public const UInt32 TOKEN_QUERY = 0x0008;

    public enum TOKEN_ELEVATION_TYPE
    {
        TokenElevationTypeDefault = 1,
        TokenElevationTypeFull,
        TokenElevationTypeLimited
    }

    public enum TOKEN_INFORMATION_CLASS
    {
        TokenUser = 1,
        TokenGroups,
        TokenPrivileges,
        TokenOwner,
        TokenPrimaryGroup,
        TokenDefaultDacl,
        TokenSource,
        TokenType,
        TokenImpersonationLevel,
        TokenStatistics,
        TokenRestrictedSids,
        TokenSessionId,
        TokenGroupsAndPrivileges,
        TokenSessionReference,
        TokenSandBoxInert,
        TokenAuditPolicy,
        TokenOrigin,
        TokenElevationType,
        TokenLinkedToken,
        TokenElevation,
        TokenHasRestrictions,
        TokenAccessInformation,
        TokenVirtualizationAllowed,
        TokenVirtualizationEnabled,
        TokenIntegrityLevel,
        TokenUIAccess,
        TokenMandatoryPolicy,
        TokenLogonSid,
        MaxTokenInfoClass  // MaxTokenInfoClass should always be the last enum 
    }

    public enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }


    public static bool IsAdmin()
    {
        var identity = WindowsIdentity.GetCurrent();
        return (null != identity && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator));
    }

    /// <summary>
    /// The function checks whether the primary access token of the process belongs
    /// to user account that is a member of the local Administrators group, even if
    /// it currently is not elevated.
    /// </summary>
    /// <returns>
    /// Returns true if the primary access token of the process belongs to user
    /// account that is a member of the local Administrators group. Returns false
    /// if the token does not.
    /// </returns>
    public static bool CanBeAdmin()
    {
        bool fInAdminGroup = false;
        IntPtr hToken = IntPtr.Zero;
        IntPtr hTokenToCheck = IntPtr.Zero;
        IntPtr pElevationType = IntPtr.Zero;
        IntPtr pLinkedToken = IntPtr.Zero;
        int cbSize = 0;

        if (IsAdmin())
            return true;

        try
        {
            // Check the token for this user
            hToken = WindowsIdentity.GetCurrent().Token;

            // Determine whether system is running Windows Vista or later operating
            // systems (major version >= 6) because they support linked tokens, but
            // previous versions (major version < 6) do not.
            if (Environment.OSVersion.Version.Major >= 6)
            {
                // Running Windows Vista or later (major version >= 6).
                // Determine token type: limited, elevated, or default.

                // Allocate a buffer for the elevation type information.
                cbSize = sizeof(TOKEN_ELEVATION_TYPE);
                pElevationType = Marshal.AllocHGlobal(cbSize);
                if (pElevationType == IntPtr.Zero)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                // Retrieve token elevation type information.
                if (!GetTokenInformation(hToken,
                    TOKEN_INFORMATION_CLASS.TokenElevationType, pElevationType, cbSize, out cbSize))
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                // Marshal the TOKEN_ELEVATION_TYPE enum from native to .NET.
                TOKEN_ELEVATION_TYPE elevType = (TOKEN_ELEVATION_TYPE)Marshal.ReadInt32(pElevationType);

                // If limited, get the linked elevated token for further check.
                if (elevType == TOKEN_ELEVATION_TYPE.TokenElevationTypeLimited)
                {
                    // Allocate a buffer for the linked token.
                    cbSize = IntPtr.Size;
                    pLinkedToken = Marshal.AllocHGlobal(cbSize);
                    if (pLinkedToken == IntPtr.Zero)
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }

                    // Get the linked token.
                    if (!GetTokenInformation(hToken,
                        TOKEN_INFORMATION_CLASS.TokenLinkedToken, pLinkedToken,
                        cbSize, out cbSize))
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }

                    // Marshal the linked token value from native to .NET.
                    hTokenToCheck = Marshal.ReadIntPtr(pLinkedToken);
                }
            }

            // CheckTokenMembership requires an impersonation token. If we just got
            // a linked token, it already is an impersonation token.  If we did not
            // get a linked token, duplicate the original into an impersonation
            // token for CheckTokenMembership.
            if (hTokenToCheck == IntPtr.Zero)
            {
                if (!DuplicateToken(hToken, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, ref hTokenToCheck))
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }

            // Check if the token to be checked contains admin SID.
            WindowsIdentity id = new WindowsIdentity(hTokenToCheck);
            WindowsPrincipal principal = new WindowsPrincipal(id);
            fInAdminGroup = principal.IsInRole(WindowsBuiltInRole.Administrator);
        }
        catch
        {
            return false;
        }
        finally
        {
            // Centralized cleanup for all allocated resources.
            if (pElevationType != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(pElevationType);
                pElevationType = IntPtr.Zero;
            }
            if (pLinkedToken != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(pLinkedToken);
                pLinkedToken = IntPtr.Zero;
            }
        }

        return fInAdminGroup;
    }

It's adapted from an article I found online somewhere, sorry, lost the attribution.

Steve Eisner
  • 2,005
  • 23
  • 35
  • A better choice might be to rethink your design so that it doesn't need the user to be an admin. Perhaps you use a windows service of some kind has admin rights. The problem is, with an ASP app, you're exposing admin rights to the web user, which could be bad if a flaw exists in your code that lets people execute arbibary code. – Erik Funkenbusch Apr 20 '10 at 18:49
  • There was no way to avoid it. But you're right, it's a good idea to wrap only the code that needs admin access in an "elevate / undo" so there's no exposure. (which is what I did) – Steve Eisner Apr 20 '10 at 23:02
  • @SteveEisner, what if UAC has been disabled? According to my tests, disabling UAC still masquerades the user in the administrator role as a standard user. I would think disabling UAC would allow for XP-Like functionality (and enhance compatibility with non-UAC compliant programs as it states in the UAC settings dialog). – Ryan Griffith Jan 15 '14 at 17:32
  • @RyanGriffith you could be right that it's not technically a UAC thing. Sorry, it's been years since I handled that issue & I don't have access to the code any more. But I was on a machine with UAC disabled when I wrote it -- I don't know if the system preference to "disable UAC" only undoes part of the security feature, etc. but the code did work in that configuration. – Steve Eisner Jan 16 '14 at 12:21
  • @RyanGriffith In Windows 8+/Windows 2012+ you can ONLY disable UAC through the registry. There is a simple test to know if it is really disabled or not - can you still run the Metro apps (or whatever they are calling them now)? If you can run them, UAC is still enabled. The slider works differently in Windows 8/2012 than it did in WIndows 7. See the comments at http://blogs.msdn.com/b/hyperyash/archive/2012/07/18/disabling-user-account-control-in-windows-8.aspx for more details. – ferventcoder Feb 08 '15 at 16:33
7

This worked for me - all I needed was to check if the program had been started in an admin role:

   public static bool IsAdminRole()
    {
        AppDomain domain = Thread.GetDomain();

        domain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
        WindowsPrincipal principle = (WindowsPrincipal)Thread.CurrentPrincipal;
        return principle.IsInRole(WindowsBuiltInRole.Administrator);
    }

Hope someone finds that of use!

Mike

Michael Watts
  • 79
  • 1
  • 1
3

I found another article here on stackoverflow which tackles this another way. I adapted it into a method below. Using Windows 7, this returned true for admins, false for non-admins, and true for non-admin when 'Run as administrator'. It looks like this will only work with .Net 3.5 and XP SP2 and later, based on an initial glance at MSDN for the PrincipleContext class.

private static bool IsUserAdmin()
{
    bool isAdmin = false;

    WindowsIdentity wi = WindowsIdentity.GetCurrent();
    WindowsPrincipal wp = new WindowsPrincipal(wi);
    isAdmin = wp.IsInRole(WindowsBuiltInRole.Administrator);

    Console.WriteLine(isAdmin); // False for Windows 7 even if user is admin

    //found the code below at [http://stackoverflow.com/questions/1089046/in-net-c-test-if-user-is-an-administrative-user][1]  

    // Add reference to System.DirectoryServices.AccountManagement (Add Referemce -> .Net)
    // Add using System.DirectoryServices.AccountManagement;

    if (!isAdmin) //PrincipleContext takes a couple seconds, so I don't use it if not necessary
    {
        using (PrincipalContext pc = new PrincipalContext(ContextType.Machine, null))
        {
            UserPrincipal up = UserPrincipal.Current;
            GroupPrincipal gp = GroupPrincipal.FindByIdentity(pc, "Administrators");
            if (up.IsMemberOf(gp))
            {
                isAdmin = true;
            }
        }
    }
    Console.WriteLine(isAdmin); // True for Windows 7 if user is admin


    return isAdmin;
}
DavB.cs
  • 31
  • 1
1

Your application is not elevated. Under normal circumstances UAC strips away the "administrator-ness" of the user. If the app can only be used by admins, add a manifest that causes it to elevate so they can keep their admin-ness. If it can be used by either, your best bet is to partition to two parts, one with an elevating manifest and one without, and launch the elevated part from a button or menu item that is decorated with the shield so users won't click it if they're not admins. (On older OS's the message to put the shield on the button will be ignored.) Searching on "UAC", "partition" and "shellexecute" will be helpful.

Kate Gregory
  • 18,808
  • 8
  • 56
  • 85
0

I've used the same approach as DavB.cs: http://tieledeclercq.blogspot.be/2013/09/c-is-this-valid-administrator-that-can.html

With a few differences:

  1. The administrator could be a nested member of the local administrator group.
  2. I needed to use external credentials (not as the current user).
Tiele Declercq
  • 2,070
  • 2
  • 28
  • 39