views:

2807

answers:

4

Our workstations are not members of the domain our SQL Server is on. (They're not actually on a domain at all - don't ask).

When we use SSMS or anything to connect to the SQL Server, we use RUNAS /NETONLY with DOMAIN\user. Then we type in the password and it launches the program. (RUNAS /NETONLY does not allow you to include the password in the batch file).

So I've got a .NET WinForms app which needs a SQL connection, and the users have to launch it by running a batch file which has the RUNAS /NETONLY command-line and then it launches the EXE.

If the user accidentally launches the EXE directly, it cannot connect to SQL Server.

Right-clicking on the app and using the "Run As..." option doesn't work (presumably because the workstation doesn't really know about the domain).

I'm looking for a way for the application to do the RUNAS /NETONLY functionality internally before it starts anything significant.

Please see this link for a description of how RUNAS /NETONLY works: http://www.eggheadcafe.com/conversation.aspx?messageid=32443204&threadid=32442982

I'm thinking I'm going to have to use LOGON_NETCREDENTIALS_ONLY with CreateProcessWithLogonW

A: 

I suppose you can't just add a user for the app to sql server and then use sql authentication rather than windows authentication?

Joel Coehoorn
Nope. And I do actually want users who access this control panel to be themselves. It's like a control dashboard that shows a number of processes and allows users to trigger them, see results, etc.
Cade Roux
+5  A: 

I just did something similar to this using an ImpersonationContext. It's very intuitive to use and has worked perfectly for me.

To run as a different user, the syntax is:

using ( new Impersonator( "myUsername", "myDomainname", "myPassword" ) )
{
    // code that executes under the new context...
}

Here is the class:

namespace Tools
{
    #region Using directives.
    // ----------------------------------------------------------------------

    using System;
    using System.Security.Principal;
    using System.Runtime.InteropServices;
    using System.ComponentModel;

    // ----------------------------------------------------------------------
    #endregion

    /////////////////////////////////////////////////////////////////////////

    /// <summary>
    /// Impersonation of a user. Allows to execute code under another
    /// user context.
    /// Please note that the account that instantiates the Impersonator class
    /// needs to have the 'Act as part of operating system' privilege set.
    /// </summary>
    /// <remarks>   
    /// This class is based on the information in the Microsoft knowledge base
    /// article http://support.microsoft.com/default.aspx?scid=kb;en-us;Q306158
    /// 
    /// Encapsulate an instance into a using-directive like e.g.:
    /// 
    ///  ...
    ///  using ( new Impersonator( "myUsername", "myDomainname", "myPassword" ) )
    ///  {
    ///   ...
    ///   [code that executes under the new context]
    ///   ...
    ///  }
    ///  ...
    /// 
    /// Please contact the author Uwe Keim (mailto:[email protected])
    /// for questions regarding this class.
    /// </remarks>
    public class Impersonator :
        IDisposable
    {
        #region Public methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Constructor. Starts the impersonation with the given credentials.
        /// Please note that the account that instantiates the Impersonator class
        /// needs to have the 'Act as part of operating system' privilege set.
        /// </summary>
        /// <param name="userName">The name of the user to act as.</param>
        /// <param name="domainName">The domain name of the user to act as.</param>
        /// <param name="password">The password of the user to act as.</param>
        public Impersonator(
            string userName,
            string domainName,
            string password)
        {
            ImpersonateValidUser(userName, domainName, password);
        }

        // ------------------------------------------------------------------
        #endregion

        #region IDisposable member.
        // ------------------------------------------------------------------

        public void Dispose()
        {
            UndoImpersonation();
        }

        // ------------------------------------------------------------------
        #endregion

        #region P/Invoke.
        // ------------------------------------------------------------------

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

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern int DuplicateToken(
            IntPtr hToken,
            int impersonationLevel,
            ref IntPtr hNewToken);

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool RevertToSelf();

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

        private const int LOGON32_LOGON_INTERACTIVE = 2;
        private const int LOGON32_PROVIDER_DEFAULT = 0;

        // ------------------------------------------------------------------
        #endregion

        #region Private member.
        // ------------------------------------------------------------------

        /// <summary>
        /// Does the actual impersonation.
        /// </summary>
        /// <param name="userName">The name of the user to act as.</param>
        /// <param name="domainName">The domain name of the user to act as.</param>
        /// <param name="password">The password of the user to act as.</param>
        private void ImpersonateValidUser(
            string userName,
            string domain,
            string password)
        {
            WindowsIdentity tempWindowsIdentity = null;
            IntPtr token = IntPtr.Zero;
            IntPtr tokenDuplicate = IntPtr.Zero;

            try
            {
                if (RevertToSelf())
                {
                    if (LogonUser(
                        userName,
                        domain,
                        password,
                        LOGON32_LOGON_INTERACTIVE,
                        LOGON32_PROVIDER_DEFAULT,
                        ref token) != 0)
                    {
                        if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)
                        {
                            tempWindowsIdentity = new WindowsIdentity(tokenDuplicate);
                            impersonationContext = tempWindowsIdentity.Impersonate();
                        }
                        else
                        {
                            throw new Win32Exception(Marshal.GetLastWin32Error());
                        }
                    }
                    else
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
                else
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }
            }
            finally
            {
                if (token != IntPtr.Zero)
                {
                    CloseHandle(token);
                }
                if (tokenDuplicate != IntPtr.Zero)
                {
                    CloseHandle(tokenDuplicate);
                }
            }
        }

        /// <summary>
        /// Reverts the impersonation.
        /// </summary>
        private void UndoImpersonation()
        {
            if (impersonationContext != null)
            {
                impersonationContext.Undo();
            }
        }

        private WindowsImpersonationContext impersonationContext = null;

        // ------------------------------------------------------------------
        #endregion
    }

    /////////////////////////////////////////////////////////////////////////
}
John Rasch
I have used similar code in a custom installer - however the 'Act as part of operating system' right is very powerful and I got a lot of pushback from system admins in granting this to average users. YMMV of course
DJ
Well this code is only good for logging on locally - for remote access credentials (basically for just the SQL connection) you have to use the equivalent of the /NETONLY flag, I'm trying to nail it down right now.
Cade Roux
Cade Roux
A different logon type and provider may help: http://blogs.catalystss.com/blogs/jonathan_rupp/archive/2008/02/26/sometimes-impersonation-isn-t-enough.aspx. In original scenario, the local machine was part of the same domain, so that solution may not work here.
Jonathan
@Jonathan - Great point, I was using this to pull event log data from remote machines on the same domain
John Rasch
+2  A: 

This code is part of an RunAs class that we use to launch an external process with elevated privleges. Passing null for username & password will prompt with the standard UAC warnings. When passing a value for the username and password you can actually launch the application elevated without the UAC prompt.

public static Process Elevated( string process, string args, string username, string password, string workingDirectory )
{
    if( process == null || process.Length == 0 ) throw new ArgumentNullException( "process" );

    process = Path.GetFullPath( process );
    string domain = null;
    if( username != null )
        username = GetUsername( username, out domain );
    ProcessStartInfo info = new ProcessStartInfo();
    info.UseShellExecute = false;
    info.Arguments = args;
    info.WorkingDirectory = workingDirectory ?? Path.GetDirectoryName( process );
    info.FileName = process;
    info.Verb = "runas";
    info.UserName = username;
    info.Domain = domain;
    info.LoadUserProfile = true;
    if( password != null )
    {
        SecureString ss = new SecureString();
        foreach( char c in password )
            ss.AppendChar( c );
        info.Password = ss;
    }

    return Process.Start( info );
}

private static string GetUsername( string username, out string domain ) 
{
    SplitUserName( username, out username, out domain );

    if( domain == null && username.IndexOf( '@' ) < 0 )
     domain = Environment.GetEnvironmentVariable( "USERDOMAIN" );
    return username;
}
Paul Alexander
The ProcessStartInfo (and therefore the Process.Start method) doesn't have any equivalent settings to RUNAS /NETONLY, where the network credentials are used only for the network connection, not for the local thread/process permissions.
Cade Roux
Bummer...you may have to resort to PInvoke and CreateProcess.
Paul Alexander
+2  A: 

I gathered these useful links:

http://www.developmentnow.com/g/36_2006_3_0_0_725350/Need-help-with-impersonation-please-.htm

http://blrchen.spaces.live.com/blog/cns!572204F8C4F8A77A!251.entry

http://geekswithblogs.net/khanna/archive/2005/02/09/22430.aspx

http://msmvps.com/blogs/martinzugec/archive/2008/06/03/use-runas-from-non-domain-computer.aspx

It turns out I'm going to have to use LOGON_NETCREDENTIALS_ONLY with CreateProcessWithLogonW. I'm going to see if I can have the program detect if it has been launched that way and if not, gather the domain credentials and launch itself. That way there will only be one self-managing EXE.

Cade Roux