views:

49

answers:

1

Context

Context first - issues I'm trying to resolve are below.

[EDIT] The application in questions is built against .NET 3.5 SP1.

One of our clients has asked as to quote how long it would take for us to improve one of our applications. This application currently provides basic user authentication in the form of username/password combinations. This client would like the ability for their employees to log-in using the details of whatever Windows User account is currently logged in at the time of running the application.

It's not a deal-breaker if I tell them no - but the client might be willing to pay the costs of development to add this feature to the application. It's worth looking into.

Based on my hunting around, it seems like storing the user login details against Domain\Username will be problematic if those details are changed. But Windows User SID's aren't supposed to change at all. I've got the impression that it would be best to record Windows Users by SID - feel free to relieve me of that if I'm wrong.

I've been having a fiddle with some Windows API calls. From within C#, grabbing the current user's SID is easy enough. I can already take any user's SID and process it using LookupAccountSid to get username and domain for display purposes. For the interested, my code for this is at the end of this post.

That's just the tip of the iceberg, however. The two issues below are completely outside my experience. Not only do I not know how to implement them - I don't even known how to find out how to implement them, or what the pitfalls are on various systems.

Any help getting myself aimed in the right direction would be very much appreciated.

Issue 1)

Getting hold of the local user at runtime is meaningless if that user hasn't been granted access to the application. We will need to add a new section to our application's 'administrator console' for adding Windows Users (or groups) and assigning within-app permissions against those users.

Something like an 'Add Windows User Login' button that will raise a pop-up window that will allow the user to search for available Windows User accounts on the network (not just the local machine) to be added to the list of available application logins.

If there's already a component in .NET or Windows that I can shanghai into doing this for me, it would make me a very happy man.

Issue 2)

I also want to know how to take a given Windows User SID and check it against a given Windows User Group (probably taken from a database). I'm not sure how to get started with this one either, though I expect it to be easier than the issue above.

For the Interested

[STAThread]
static void Main(string[] args)
{
    MessageBox.Show(WindowsUserManager.GetAccountNameFromSID(WindowsIdentity.GetCurrent().User.Value));
    MessageBox.Show(WindowsUserManager.GetAccountNameFromSID("S-1-5-21-57989841-842925246-1957994488-1003"));
}

public static class WindowsUserManager
{
    public static string GetAccountNameFromSID(string SID)
    {
        try
        {
            StringBuilder name = new StringBuilder();
            uint cchName = (uint)name.Capacity;
            StringBuilder referencedDomainName = new StringBuilder();
            uint cchReferencedDomainName = (uint)referencedDomainName.Capacity;
            WindowsUserManager.SID_NAME_USE sidUse;

            int err = (int)ESystemError.ERROR_SUCCESS;
            if (!WindowsUserManager.LookupAccountSid(null, SID, name, ref cchName, referencedDomainName, ref cchReferencedDomainName, out sidUse))
            {
                err = Marshal.GetLastWin32Error();
                if (err == (int)ESystemError.ERROR_INSUFFICIENT_BUFFER)
                {
                    name.EnsureCapacity((int)cchName);
                    referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);

                    err = WindowsUserManager.LookupAccountSid(null, SID, name, ref cchName, referencedDomainName, ref cchReferencedDomainName, out sidUse) ?
                        (int)ESystemError.ERROR_SUCCESS :
                        Marshal.GetLastWin32Error();
                }
            }

            if (err != (int)ESystemError.ERROR_SUCCESS)
                throw new ApplicationException(String.Format("Could not retrieve acount name from SID. {0}", SystemExceptionManager.GetDescription(err)));

            return String.Format(@"{0}\{1}", referencedDomainName.ToString(), name.ToString());
        }
        catch (Exception ex)
        {
            if (ex is ApplicationException)
                throw ex;

            throw new ApplicationException("Could not retrieve acount name from SID", ex);
        }
    }

    private enum SID_NAME_USE
    {
        SidTypeUser = 1,
        SidTypeGroup,
        SidTypeDomain,
        SidTypeAlias,
        SidTypeWellKnownGroup,
        SidTypeDeletedAccount,
        SidTypeInvalid,
        SidTypeUnknown,
        SidTypeComputer
    }

    [DllImport("advapi32.dll", EntryPoint = "GetLengthSid", CharSet = CharSet.Auto)]
    private static extern int GetLengthSid(IntPtr pSID);

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool ConvertStringSidToSid(
                string StringSid,
                out IntPtr ptrSid);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool LookupAccountSid(
      string lpSystemName,
      [MarshalAs(UnmanagedType.LPArray)] byte[] Sid,
      StringBuilder lpName,
      ref uint cchName,
      StringBuilder ReferencedDomainName,
      ref uint cchReferencedDomainName,
      out SID_NAME_USE peUse);

    private static bool LookupAccountSid(
        string lpSystemName,
        string stringSid,
        StringBuilder lpName,
        ref uint cchName,
        StringBuilder ReferencedDomainName,
        ref uint cchReferencedDomainName,
        out SID_NAME_USE peUse)
    {
        byte[] SID = null;
        IntPtr SID_ptr = IntPtr.Zero;
        try
        {
            WindowsUserManager.ConvertStringSidToSid(stringSid, out SID_ptr);

            int err = SID_ptr == IntPtr.Zero ? Marshal.GetLastWin32Error() : (int)ESystemError.ERROR_SUCCESS;

            if (SID_ptr == IntPtr.Zero ||
                err != (int)ESystemError.ERROR_SUCCESS)
                throw new ApplicationException(String.Format("'{0}' could not be converted to a SID byte array. {1}", stringSid, SystemExceptionManager.GetDescription(err)));

            int size = (int)GetLengthSid(SID_ptr);
            SID = new byte[size];

            Marshal.Copy(SID_ptr, SID, 0, size);
        }
        catch (Exception ex)
        {
            if (ex is ApplicationException)
                throw ex;

            throw new ApplicationException(String.Format("'{0}' could not be converted to a SID byte array. {1}.", stringSid, ex.Message), ex);
        }
        finally
        {
            // Always want to release the SID_ptr (if it exists) to avoid memory leaks.
            if (SID_ptr != IntPtr.Zero)
                Marshal.FreeHGlobal(SID_ptr);
        }

        return WindowsUserManager.LookupAccountSid(lpSystemName, SID, lpName, ref cchName, ReferencedDomainName, ref cchReferencedDomainName, out peUse);
    }
}
+2  A: 

If you're using the 3.5 version of the framework, you really want to look into System.DirectoryServices.AccountManagement. I've used it before to provide lookups of AD accounts, and it's much simpler to deal with than writing your own class. It will also solve your #2 question. I don't have the code at hand, but if you need it I can always look it up.

Harper Shelby
+1 Built in .NET libraries should always be looked at before rolling your own.
Sekhat
Yep, we're using 3.5 - edited the main post to reflect this. Thanks for the namespace, I'll have an investigate and come back later.
Ubiquitous Che