CreateProcessAsUser

Friday, October 29, 2004 by scott
31 comments

Here is a solution you can use in rare circumstances and with caution. Comments are more then welcomed.

Anytime someone approaches me with a design requiring the creation of a server side process – I flinch. Spinning up a process on the server is something to avoid. Having said that, I found it useful in a small Intranet applications (a build engine) to put a web service / ASP.NET interface in front of some command line CM tools. The interface is available to a small number of trusted individuals.

I wanted to run these processes with the identity of the client, but this poses a problem. The Process class in System.Diagnostics can start a new process, but the process always inherits the security context of the parent process. Even if the ASP.NET thread invoking the Start method is impersonating a client, the Process still starts with the ASP.NET worker process credentials.

Enter .NET 2.0, which includes the User, Domain, and Password properties on the ProcessStartInfo type. In .NET 2.0 you can start a process under a different set of credentials. The catch is having a password to give. I don't want the burden of managing passwords when there is a domain controller handy.

Having found no solution in the framework, the next step was to look into the Win32 API. There are four basic functions to start a process: CreateProcess, CreateProcessWithLogonW, CreateProcessAsUser, and CreateProcessWithTokenW. Since CreateProcess doesn’t allow an alternate identity, and one of 11 parameters to CreateProcessWithLogonW is a password, those are both out of the running.

The remaining two (CreateProcessWithTokenW and CreateProcessAsUser) both accept a token instead of a username and password, so these look promising. CreateProcessWithTokenW allows greater fine tuning of the logon type and creation flags, which I didn't need, so I focused in on CreateProcessAsUser.

CreateProcessAsUser accepts a user token as the first parameter. We can easily get a token representing the client we are impersonating using the WindowsIdentity class in .NET, but there still exists a problem. The token will be an impersonation token - which isn’t good enough for CreateProcessAsUser. However, the docs mention that you can use DuplicateTokenEx to convert an impersonation token into a primary token. The code at the end of this post demonstrates the incantations.

Note: The calling process needs SeAssignPrimaryTokenPrivilege and SeIncreaseQuotasPrivilege privileges, which the NETWORK SERVICE account has by default on Windows 2003.

Note: I’m not sure how far the ‘primary token’ yielded by DuplicateTokenEx can go. Could I effectively delegate without delegation enabled? I wouldn't think so. Must experiment. Anyone know?

Note: If you are thinking of using this to launch an interactive GUI application on the server – don’t. The process will start in a non-interactive window station and remain invisible but consuming memory.

 

private void CreateProcessAsUser()
{
   IntPtr hToken = WindowsIdentity.GetCurrent().Token;         
   IntPtr hDupedToken = IntPtr.Zero;        
   
   ProcessUtility.PROCESS_INFORMATION pi = new ProcessUtility.PROCESS_INFORMATION();
   
   try
   {
      ProcessUtility.SECURITY_ATTRIBUTES sa = new ProcessUtility.SECURITY_ATTRIBUTE();
      sa.Length = Marshal.SizeOf(sa); 
 
      bool result = ProcessUtility.DuplicateTokenEx(
            hToken, 
            ProcessUtility.GENERIC_ALL_ACCESS,
            ref sa, 
            (int)ProcessUtility.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
            (int)ProcessUtility.TOKEN_TYPE.TokenPrimary, 
            ref hDupedToken
         );
   
      if(!result)
      {               
         throw new ApplicationException("DuplicateTokenEx failed");
      }
 
 
      ProcessUtility.STARTUPINFO si = new ProcessUtility.STARTUPINFO();
      si.cb = Marshal.SizeOf(si);
      si.lpDesktop = String.Empty;           
 
      result = ProcessUtility.CreateProcessAsUser(
                           hDupedToken, 
                           @"",
                           String.Empty,
                           ref sa, ref sa, 
                           false, 0, IntPtr.Zero, 
                           @"C:\", ref si, ref pi
                     );
 
      if(!result)
      {  
         int error = Marshal.GetLastWin32Error();
         string message = String.Format("CreateProcessAsUser Error: {0}", error);
         throw new ApplicationException(message);
      }
   }
   finally
   {
      if(pi.hProcess != IntPtr.Zero)
         ProcessUtility.CloseHandle(pi.hProcess);
      if(pi.hThread != IntPtr.Zero)
         ProcessUtility.CloseHandle(pi.hThread);
      if(hDupedToken != IntPtr.Zero)
         ProcessUtility.CloseHandle(hDupedToken);      
   }
}

 

ProcessUtility…

 

public class ProcessUtility
{
   [StructLayout(LayoutKind.Sequential)]
      public struct STARTUPINFO
   {
      public Int32 cb;
      public string lpReserved;
      public string lpDesktop;
      public string lpTitle;
      public Int32 dwX;
      public Int32 dwY;
      public Int32 dwXSize;
      public Int32 dwXCountChars;
      public Int32 dwYCountChars;
      public Int32 dwFillAttribute;
      public Int32 dwFlags;
      public Int16 wShowWindow;
      public Int16 cbReserved2;
      public IntPtr lpReserved2;
      public IntPtr hStdInput;
      public IntPtr hStdOutput;
      public IntPtr hStdError;
   }
 
   [StructLayout(LayoutKind.Sequential)]
      public struct PROCESS_INFORMATION
   {
      public IntPtr hProcess;
      public IntPtr hThread;
      public Int32 dwProcessID;
      public Int32 dwThreadID;
   }
 
   [StructLayout(LayoutKind.Sequential)]
      public struct SECURITY_ATTRIBUTES
   {
      public Int32 Length;
      public IntPtr lpSecurityDescriptor;
      public bool bInheritHandle;
   }
 
   public enum SECURITY_IMPERSONATION_LEVEL
   {
      SecurityAnonymous,
      SecurityIdentification,
      SecurityImpersonation,
      SecurityDelegation
   }
 
   public enum TOKEN_TYPE
   {
      TokenPrimary = 1, 
      TokenImpersonation
   } 
 
   public const int GENERIC_ALL_ACCESS = 0x10000000;
 
   [
      DllImport("kernel32.dll", 
         EntryPoint = "CloseHandle", SetLastError = true, 
         CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
   ]
   public static extern bool CloseHandle(IntPtr handle);
 
   [
      DllImport("advapi32.dll", 
         EntryPoint = "CreateProcessAsUser", SetLastError = true, 
         CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
   ]
   public static extern bool 
      CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, 
                          ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, 
                          bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment,
                          string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, 
                          ref PROCESS_INFORMATION lpProcessInformation);
 
   [
      DllImport("advapi32.dll", 
         EntryPoint = "DuplicateTokenEx")
   ]
   public static extern bool 
      DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess, 
                       ref SECURITY_ATTRIBUTES lpThreadAttributes,
                       Int32 ImpersonationLevel, Int32 dwTokenType, 
                       ref IntPtr phNewToken);
}

Resolving SSL Error Messages With Reporting Services

Friday, October 29, 2004 by scott
1 comment

A couple people have left comments here about running into the following error in Reporting Services:

The underlying connection was closed: Could not establish secure channel for SSL/TLS.

There are a number of reasons this error can occur, so it will involve some digging to get to the root cause. Most of the problems revolve around a failure to verify the SSL certificate on the report server. You might see the error because:

  • The name for the certificate does not match the name of the server
  • The certificate has expired or is considered invalid.

A good way to troubleshoot is to browse to the report server with Internet Explorer (https://machinename/reportserver) and see if IE complains.

There are also some less obvious reasons the error might occur. For example, the system time might be incorrect - making it appear as if the certificate has expired (when it has not), or has not yet been created (a certificate from the future!). Set the clock correctly.

If IE likes the cert, there might be a configuration issue. The URL used by the Report Manager to communicate with the ReportServer is found in a file under the Report Manager directory by the name of RSWebApplication.config. Find the <ReportServerUrl> element and make sure the URL matches the name on the certificate (which will match the name of the server). Hint: using https://localhost/reportserver is not going to work.

If you are in an environment that does not require SSL, and you are 100% positive you want payroll reports to cross the ether in plain text, you can find the RSReportServer.config file in the ReportServer directory and set SecureConnectionLevel Value=”0”.

I’m sure this is not the exhaustive list, but hopefully it will help someone out.

Success Is Sweet!!

Wednesday, October 27, 2004 by scott
2 comments

My impatience rushed me into tempting fate. Instead of waiting for the full moon to rise tomorrow evening, I started my BizTalk install Sunday afternoon. (There is also a lunar eclipse tomorrow evening - a blood moon, visible everywhere except from Australia - sorry John).

I’m happy to say the installation went swimmingly. The HelloWorld Orchestration sample spit out an invoice, and much celebration ensued.

So, what are the keys to a successful BizTalk installation? Let me tell you what worked for me.

1: Read John Elliot’s incredibly clever and Shakespearean inspired ‘Installing BizTalk 101’. (Sorry, that link is no longer available. Perhaps John will send me the contents....)

2: Follow Luke Nyswonger’s “The QuickStart Guide to Installing & Configuring Microsoft BizTalk Server 2004” step by step. Let me quote prose from Ray Bradbury’s well-known short story “A Sound Of Thunder”:

Stay on the path. Don’t go off it. I repeat. Don’t go off it. For any reason! If you fall off, there’s a penalty.”

The following steps are optional, but comforting if you are the suspicious sort.

3: I took Anil’s suggestion and added a candle to the mix. The candle I used carried the delicate scent of a blueberry harvest. I feel fairly confident in recommending any fruit scented candle, but avoid vanilla, cinnamon, and wild honysuckle.

(Note: be sure not to use those cheap little birthday candles because the process takes a while. You’ll need to sustain a good 3-6 hour burn. Also, remember to check the fire codes first if you are conducting the ritual inside a place of business).

4. I also placed a small Ganesh (remover of obstacles) figurine on the target machine. Figure 1 will give you an idea of how to arrange the candle and charm to maximize your chances of success.

Figure 1: BizTalk installations should place candles on the left - good luck charms on the right.

P.S. In all seriousness, thanks to Lee and Luke for offering to help out and giving me some pointers. 

I Didn’t Expect These Features

Monday, October 25, 2004 by scott
2 comments

What do MARS and the new Edit and Continue features have in common?

  • Both were technically difficult to implement
  • I never expected either to show up.

Whenever discussions cropped up about these features the pushback always seemed to indicate both required major disruptions to the lowest level code you can imagine, and possibly changing the gravitational constant. Now these features are here in beta.

Edit and continue (E&C) allows a developer to edit code in a running application and continue running the updated application without having to stop, compile, and launch the debugger again. It’s amazing the range of responses this feature generates. Jonathon Goodyear proclaimed it would be a colossal mistake for Microsoft to not ship this feature in 2005. Sam Gentile (who is a C# MVP, just in case he didn’t tell you), feels E&C will ruin software development.

In the end these discussions always collapse into some sort of three way language war between people who use semicolons, people who don’t use semicolons, and people who use Smalltalk. It’s odd how that happens. I wonder what happens inside the meeting rooms around Redmond when this feature came up for discussion?

I don’t see myself ever using E&C, except maybe once every two years when some weird bug crops up in a windows service with a long setup time. Given it has taken 4 or more years to work this hot potato into the runtime, I’m sure I would have picked other features I could use in day to day work and never miss E&C.

At least with E&C you can take it or leave it. MARS, on the other hand, seems to be sneaking in with an impact on anyone using the native .NET SQL Server client.

Multiple Active Result Sets (MARS) lets a client have more than one pending request on a given connection. What does this mean in code? Consider the following snippet:

SqlConnection connection = new SqlConnection("[connection string]");
connection.Open();
SqlCommand command = new SqlCommand("[query 1]", connection);
SqlDataReader reader = command.ExecuteReader();
SqlCommand command2 = new SqlCommand("[query 2]", connection);
SqlDataReader reader2 = command2.ExecuteReader();

I just ran this with the October CTP of Whidbey and SQL Server. The application runs without error. In .NET 1.1, however, we would see an exception:

Unhandled Exception: System.InvalidOperationException: There is already an open
DataReader associated with this Connection which must be closed first.

The Technet document introducing MARS gives explanations and use cases for where this behavior is useful, and it also mentions: “In conclusion, we can say that MARS is all about getting rid of "Connection Busy", a significant improvement to the programming model.”. It does sound attractive on the surface, but…

You can ask anyone in the ASP.NET or ADO.NET newsgroups (like Bill, for instance), and they can tell you the aforementioned exception shows up time and time again in questions. The exception is usually the first clue a new developer has that they are not closing database connections. Oh well, you say, they will just have to learn the hard way, what impact is in it for me?

For starters, make sure to read the sections on “Performance and Cost Considerations” and “MARS Deadlocks” as food for thought. Even more interesting is the MARS FAQ on Angel Saenz-Badillos blog. Suddenly, creating a SqlCommand isn’t going to be quick and relatively free (and the OleDb provider has all sorts of caveats to watch for). MARS is on by default, and there really is no way to disable the feature to recover the cost or avoid the worst case scenarios for the feature.

ADO.NET seems to be replanting new pitfalls over the old ones we left behind in ADO. Surely, there must be some driving reason for this complexity in ADO.NET.

Could it be something required to build ObjectSpaces?

SetReportParameters

Friday, October 22, 2004 by scott
8 comments

Question came in today about the SetReportParameters web service method of Reporting Services.

The name SetReportParameters is a bit misleading, and the documentation isn’t extremely clear. The method does not pass parameters to a report for rendering. Instead, the method has the same effect as modifying the parameter properties in the report designer. You can programmatically set valid values and prompt strings, etc. SetReportParameters is more of a “design time” method.

When SSRS goes to render a report, the parameter values come from one of three sources.

First, there could be a default parameter value specified in the report definition. If a default value exists SSRS will use the value, but you can override the default by passing a new parameter value using one of the techniques in the second source of parameter values.

The second source of a parameter value depends on how you access the Report Server. With URL access, you can pass a parameter value in the query string (&ParamName=Europe). If you are using the web service, then the ReportingService.Render web method accepts an array of type ParameterValue. The documentation online includes an example of how to pass the parameter values.

Finally, if there is a parameter left that does not have a default value, and you did not specify a value in the query string or in the web service call, SSRS has no choice but to prompt the user to enter a value. If you have hidden the parameter input area or the toolbar, the Report Server will throw an exception, otherwise you’ll see the TextBox and DropDownList controls appear for the user to select values.

Hope this helps…

The Lies I Tell At Work

Thursday, October 21, 2004 by scott
14 comments

I’m glad you are here, gentle reader. I hope you’ve arrived in an understanding mood, because I feel compelled to unburden myself at your expense. For the last four months, I’ve practiced the art of deception at work.

You see, every few weeks our CEO brings up the topic of BizTalk Server 2004. We talk vaguely about how BizTalk might help us deploy solutions for our clients. A discussion ensues about XML and healthcare schemas. Eventually, someone will ask me if I’d had a chance to investigate the technology more thoroughly.

“Well”, I say, “the Canadian project we picked up has required more of my attention then I originally thought. I have not had a chance to dig into BizTalk yet”.

Not exactly true.

Other times I say: “I’ve been working with the statistician on algorithms for severity adjustment of patient diagnoses, but I’ll try to squeeze BizTalk into the schedule real soon”.

A little fib.

My dear reader, I’m about to give you the truth of the matter. Here is the real reason I have not devoted my research time to this marvelous product known as BizTalk Server.

I CAN’T GET THE @##@*^% PIECE OF @^*% TO INSTALL AND I’VE TRIED FIFTEEN #&#*^% TIMES AND I’M AFRAID THE BOSS WILL THINK I’M SOME SORT OF #&#**^% DIMWIT.

There. It’s off my chest. It’s in the open.

Ok, technically, it “installs”, but I’m impotent when it comes to running the configuration wizard. I’ve tried on multiple machines. I’ve tried with fewer features. Not once have I made it through ConfigFramework.exe without a severe error. BizTalk hates me, but I have a plan.

One week from today, on October 27th, will bring a full moon. I know where I can buy some chickens, and I found a 1-900 hotline with virgins who say they will do anything. In the evening of October 27th - I will try once again to install BizTalk Server 2004.

I’m feeling better now, having admitted my hideous ruse to you, dear reader. I hope you don’t think any less of me than you already did.

October 27th.

Stay tuned for details.

Evaluating The DataBinder

Wednesday, October 20, 2004 by scott
2 comments

I was trying to put together a quick piece of code as an example over the weekend and remembered how much I dislike using DataBinder in ASP.NET.

Let’s say a web service call gives back an array of simple objects (the sort of objects you’d see imported by a web reference):

class Parameter
{  
 public string Name;
 public string Value;
}

I wanted to dump the Parameter array to a web page as easily as possible. The following will not work.

<asp:Repeater id="Repeater1" runat="server">
   <ItemTemplate>
      <tr>
         <td>
            <%# DataBinder.Eval(Container.DataItem, "Name") %>
         </td>
         <td>
            <%# DataBinder.Eval(Container.DataItem, "Value") %>
         </td>
      </tr>
   </ItemTemplate>
</asp:Repeater>

The DataBinder, besides looking awkward, only finds public properties - it doesn’t find public fields. The quick hack to get around this is to replace the language agnostic DataBinder syntax with C# code:

<asp:Repeater id="Repeater1" runat="server">
    <ItemTemplate>
        <tr>
            <td>
                <%# ((Parameter)(Container.DataItem)).Name %>
            </td>
            <td>
                <%# ((Parameter)(Container.DataItem)).Value %>
            </td>
        </tr>
    </ItemTemplate>
</asp:Repeater>

Disadvantages:
There is a huge disadvantage in that if the Parameter class fields ever change, the code in the ASPX won’t generate an error until runtime. It’s also a problem if the ASPX moved from a C# project to a VB.NET project.

Advantages:
To me the second example looks cleaner – I can see an object and a property even through the parentheses of a cast. It’s also blazingly faster - two order of magnitude faster. In fact, in my tests the following:

<%# DataBindParameterName(Container.DataItem) %>
// in the code behind of the class:
protected string DataBindParameterName(object o)
{
    return ((Parameter)o).Name;
}

is still 300x faster than the reflection machinations DataBinder.Eval uses. Food for thought in perf critical scenarios.

by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!