Thursday, March 11, 2010

Protocol Transition with BizTalk

 

Background

A customer had a problem; they wanted a web service that was called by user UserA to run a BizTalk orchestration and at the end of the orchestration another web service would be called and they wanted that to be executed in the context of the caller.
image 

Fig 1: Illustration of problem

Above, the user token is lost when we enter the BizTalk orchestration. BizTalk will call any external services as the BizTalk process account. Setting up Kerberos will not fix this. This problem is not the typical double hop issue.

Workaround with Trusted sub system 

This problem can be tacked with the trusted subsystem model. The caller (Biztalk) sets the username in a header and the receiver acts as this user. Note that Biztalk does not call the endpoint using the callers credentials; It merely passes the username as a string. The web services must trust that if they are called from the BizTalk server the user parameter is correct. The web services are using API’s that allow impersonation. The effort to get this working is placed on both BizTalk and the endpoint, we have to pass the username around and we have to act as this user on the receiver.

For example
User calls service a as user UserA
BizTalk gets the username by using BTS.WindowsUser.
BizTalk calls the endpoint and passes the username.
The APIs that are used in the backend system behind the endpoint impersonate UserA (Note; no operating system level impersonation is done)

To make the above secure, the endpoint only accepts calls from the BizTalk server and it trusts the claims it makes.

Workaround with Constrained delegation

Another way to tackle this problem is to use constrained delegation. The works as follows :
User calls service a as user UserA

BizTalk gets the username by using BTS.WindowsUser.
BizTalk calls the endpoint and inserts the username in the message context.
A WCF adapter intercepts message and impersonates the user at the operating system level. (We are still on the BizTalk server at this point)
The receiving WCF endpoint is now running in the context of the user.
Why is this safe? How can you just impersonate a user? Without the password? Well you can and it’s safe (please read )

Trusted subsystem vs Constrained delegation

A simple way to think of this is, with Constrained delegation you put the effort into the BizTalk and the infrastructure hosting the BizTalk. With trusted sub system you put the effort into the receiver.

Please note, that the term “trusted sub system” can be used to describe other solutions to similar security related problems.

If you OWN and control all the endpoints then I would consider using the trusted sub system model. Your dev team may find this easier to get working and does not rely on setting up your domain. Saying that, once you do get this working you will probably find it easier to add more systems along the way.

How to

The solution that has been built by following these steps can be found on codeplex. I tend to-do things the TDD way (as much possible) this goes for BizTalk too. I tend not to drop files to test things, I hit web services and expect things to get returned. This step by step guide will go through creating the web services that call BizTalk.

Prerequisites
  • Visual Studio 2008.
  • BizTalk 2009 Installed and working.
  • Domain Admin privileges.
  • Access to setspn. (Comes with windows 2003 support tools, I think it’s built into 2008 r2)
  • An account to delegate to (just ask a colleague if you can impersonate them).
  • Windows 2008 R2 (That's what I used, you can get this working with other versions but things may not be exactly the same).
  • A Windows 2008 R2 Server to host the final WCF service. (To test properly, this NEEDS to be a separate server or VM)
Step One (Setup Delegation)

running as a domain admin run the following SETSPN commands
Setup for BIZTALK server running as DOM\BizTalkUser




setspn –A HTTP/BIZTALKSERVER DOM\BizTalkUser 
setspn –A HTTP/BIZTALKSERVER.FQDN DOM\BizTalkUser




Setup for WCF server running as network system.















setspn –A HTTP/WCFSERVER WCFSERVER 
setspn –A HTTP/WCFSERVER.FQDN WCFSERVER




Note, .FQDN is the full qualified domain name, eg server.domain.local





On the BIZTALK server ensure the account has the “act as part of operating system” right.





Using AD enable constrained delegation from the user DOM\Biztalk to WCFSERVER over HTTP. Edit settings on the Biztalk server. You will need to do this for the account. If you do not see the delegate options on the account this means the spns have not been created.





image 


Step Two (Create the solution)

Create a blank solution in Visual Studio 2008 called LetsDoProtocolTransition. Add it empty tests project to this. Call this Tests (or what ever you wish)







Step Three (create EndPoint system and test constrained delegation works)


We will be creating a WCF web service and some tests to call this. One test will impersonate the other will not.



  • Select Add New Web Site, select WCF Service, set location to HTTP and call this http://localhost/WCFStartBiztalk 


  • Delete Service.cs and IService.cs.


  • Using add new item, select WCF Service, call this NameService


  • Open INameService, remove



    [OperationContract] 
    void DoWork();



  • Add



    [OperationContract] 
    string WhatsLoginName();



  • Implement, just have it return “” for now as we are doing this the TDD way :)


Build and add a reference to this service from your test project. Enter WCFBackEndSystem in the namespace.


  • Add the following settings to your tests (right click on project, select settings to get to this screen) :

    image


  • Add a test file to your test project, name this file WCFBackEndTest.cs add the following code to your test :
    [TestMethod]
    public void SimpleCall()
    {
    NameServiceClient client = new NameServiceClient();
    Assert.AreEqual(WindowsIdentity.GetCurrent().Name, client.WhatsLoginName(), true);
    }



  • Run the tests and you will get Assert.AreEqual failed. Expected:<DOM\User>. Actual:<>.


  • Implement the backend code :



    public string WhatsLoginName() 
    {
    return OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Name;
    }



  • Run your test and it should pass.


  • Add a new tests with the following code
    [TestMethod]
    public void SimpleCallAsUser()
    {
    WindowsIdentity wi = new WindowsIdentity(Tests.Properties.Settings.Default.remoteUPN);
    WindowsImpersonationContext imp = wi.Impersonate();
    NameServiceClient client = new NameServiceClient();


    Assert.AreEqual(Tests.Properties.Settings.Default.remoteUser , wi.Name, true,
    "Could not impersonate, make sure you can act as part of operating system");
    Assert.AreEqual(Tests.Properties.Settings.Default.remoteUser, client.WhatsLoginName(), true,
    "Call to service failed, delegation not working");
    imp.Undo();
    }



  • Run your test and it should pass.


  • If you get Call to service failed, then you don't have delegation setup correctly. Or…. you have problems with SPNs, you may need to get out ADSI edit have a look around.


  • The above tests should work, we are not calling a remote server but we are proving that part of this works.


  • Copy the WCF service to the remote server and create a new test that looks like
    [TestMethod]
    public void RemoteCall()
    {
    NameServiceClient client = new NameServiceClient();
    client.Endpoint.Address = new EndpointAddress(Settings.Default.WCFService);
    Assert.AreEqual(WindowsIdentity.GetCurrent().Name, client.WhatsLoginName(), true);
    }

    [TestMethod]
    public void RemoteCallAsUser()
    {
    NameServiceClient client = new NameServiceClient();
    client.Endpoint.Address = new EndpointAddress(Settings.Default.WCFService);

    WindowsIdentity wi = new WindowsIdentity(Settings.Default.remoteUPN);
    WindowsImpersonationContext imp = wi.Impersonate();

    Assert.AreEqual(Settings.Default.remoteUser, wi.Name, true, "Could not impersonate, make sure you can act as part of operating system");
    Assert.AreEqual(Settings.Default.remoteUser, client.WhatsLoginName(), true, "Call to service failed, delegation not working");

    imp.Undo();
    }



if everything is setup correctly you should get all greens.

Step Four (The BizTalk bits)

For this part, lets assume the following understanding of biztalk :


  • How to create a biztalk orchestration that is created by calling a SOAP web service.


  • How to consume a WCF service.


  • How to setup the ports for the above service.


I assume that you may not have done the following :


  • Passed calling user name to a header


  • Installed a WCF extension


  • Editing the custom binding


Our incoming message is the same as the outgoing to keep this simple. This message has the CallerName, BizTalk Username and the WCF Service Name. They should all match. The XML will look like :

<ns0:userDetails callerClaim="DOM\ExampleUser" biztalkClaim="DOM\ExampleUser" wcfClaim="DOM\ExampleUser" xmlns:ns0="LetsDoProtocolTransition" /> 





When I create these types of POC I tend to put exception handling into BizTalk and have it write out details using Debug.WriteLine. You can then run debug view and see the messages.





This also means if things don't work you don't get things stuck in any BizTalk queues so this makes redeploying easier. Any outgoing ports are setup with zero retries for the same reason.





Your final orchestration will look like this:








image 
When you are wiring up the WCF endpoints use Custom Bindings as we will be editing these.





We won’t be adding the custom WCF behaviour yet, we will get this working without. The WCF services will return the BizTalk process account.





Publish the orchestration as a web service with the address http://localhost/LetsDoProtocolTransition





For now, just get BizTalk calling your WCF endpoint. Don't worry about passing the user. Look at the next section for the test used to test BizTalk.



Step Five (The Tests)



  • Add a ASMX reference to http://localhost/LetsDoProtocolTransition


  • Call this BizTalkService with this code



    [TestMethod]
    public void CallBiztalk()
    {
    //
    // TODO: Add test logic here
    //
    BizTalkService.LetsDoProtocolTransition_Protocol_Transition_Orchestration_Port_1 client;
    client = new Tests.BizTalkService.LetsDoProtocolTransition_Protocol_Transition_Orchestration_Port_1();
    client.Credentials = CredentialCache.DefaultCredentials;

    WindowsIdentity wi = new WindowsIdentity(Tests.Properties.Settings.Default.remoteUPN);
    WindowsImpersonationContext imp = wi.Impersonate();

    var userDetails = new Tests.BizTalkService.userDetails();
    userDetails.callerClaim = WindowsIdentity.GetCurrent().Name;
    userDetails.biztalkClaim = "";
    userDetails.wcfClaim = "";

    client.Operation_1(ref userDetails);

    Trace.WriteLine("You are\t\t\t\t: " + userDetails.callerClaim);
    Trace.WriteLine("BizTalk thinks are\t\t\t: " + userDetails.biztalkClaim);
    Trace.WriteLine("The WCF Services thinks you are\t: " + userDetails.wcfClaim);

    Assert.IsTrue( string.Equals(userDetails.callerClaim,userDetails.biztalkClaim ,StringComparison.CurrentCultureIgnoreCase ), string.Format( "BizTalk does not agree that you are who you say you are. You Say {0}, BizTalk Says {1}", userDetails.callerClaim,userDetails.biztalkClaim ));
    Assert.IsTrue(string.Equals(userDetails.callerClaim, userDetails.wcfClaim , StringComparison.CurrentCultureIgnoreCase), string.Format("WCF does not agree that you are who you say you are. You Say {0}, BizTalk Says {1}", userDetails.callerClaim, userDetails.wcfClaim));

    imp.Undo();
    }



  • Run the test, it will fail as the WCF service will be running as the BIZTALK user.











Step Six (Adding the WCF Custom Channel)



You will need to download this from codeplex, download the project and add it to your solution. Build it (make sure it goes in the GAC).








Open your machine.config (if you running 64bit then depending on your biztalk setup you may need to edit the 64bit version). Locate










<system.serviceModel>
<extensions>







And add










<add name="protocolTransition" type="Microsoft.BizTalk.Rangers.ProtocolTransition.WCFCustomChannel.InspectingBehaviorExtensionElement, Microsoft.BizTalk.Rangers.ProtocolTransition.WCFCustomChannel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=874a60d7e5a4dd9b" />






To the behaviorExtensions node and bindingElementExtensions, this section of the config will look something like :








image











You can now use the protocol extension with WCF. This is done with a custom binding.


















Step Seven (Testing the WCF Binding)






To make testing of the WCF custom binding less effort we are going to setup the channel / binding with code. This mean you don't need to edit the config file and break the other tests. With real code I would use the config file so it would be less ‘hard coded’.











  • Add the following code :



    [TestMethod]
    public void WCFCustomChannelTest()
    {

    var textMessageEncoding = new TextMessageEncodingBindingElement();
    var protocolTransition = new InspectingBindingElement();
    var httpTransport = new HttpTransportBindingElement();

    textMessageEncoding.MessageVersion = MessageVersion.Soap11;
    httpTransport.AuthenticationScheme = System.Net.AuthenticationSchemes.Negotiate;
    protocolTransition.UserNameOverride = Settings.Default.remoteUPN ;

    CustomBinding binding = new CustomBinding(
    textMessageEncoding,
    protocolTransition,
    httpTransport
    );

    var channelFactory = new System.ServiceModel.ChannelFactory<INameService>(binding);
    INameService client = channelFactory.CreateChannel(new EndpointAddress(Settings.Default.WCFService));

    Assert.AreEqual(Settings.Default.remoteUser, client.WhatsLoginName(), true);
    }






  • If you run debug view you will see trace information.




    image


  • or double click on the test and you will see :




    image







We have now proved that our WCF Extension is intercepting the call and impersonating a user.










Step Eight (Getting it working from Biztalk)






To get this to work from BizTalk we need to modify the port that biztalk sends the message through. Assuming you have got the BizTalk bits working.











  • Create an XML file that contains the following (naming it CustomBindingConfig.config) :



    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
    <system.serviceModel>
    <client>
    <endpoint address="http://<YOURSERVER>/WCFBackendSystem/NameService.svc" binding="customBinding" bindingConfiguration="customBinding" contract="BizTalk" name="WcfSendPort_NameService_BasicHttpBinding_INameService_Custom" />
    </client>
    <bindings>
    <customBinding>
    <binding name="customBinding">
    <textMessageEncoding messageVersion="Soap11" />
    <protocolTransition ContextPropertyName="WindowsUser" ContextPropertyNamespace="http://schemas.microsoft.com/BizTalk/2003/system-properties" />
    <httpTransport authenticationScheme="Negotiate">
    <extendedProtectionPolicy policyEnforcement="Never" />
    </httpTransport>
    </binding>
    </customBinding>
    </bindings>
    </system.serviceModel>
    </configuration>



  • In your send ports, select the WCF one



  • image





  • Select configure, select import (on the import/export tab).


  • Import the xml config file created above.


  • You should see the following binding details , ok these




    image


  • Run your tests, you should get all greens.




    image


  • The output of the biztalk tests should be something like :




    image


  • As you can see we are running tests as wisdomv6 but all the checks return the user that we are impersonating.




What have we just done?



In simple terms we have extended WCF to allow it to impersonate a user before it calls the endpoint. We have configure our domain to allow impersonation. We have passed calling user to the WCF EndPoint in BizTalk.

Tuesday, December 15, 2009

WCF, Duplex Channels, firewalls and NATs


Introduction

WCF supports duplex channels; this means you can have a server fire an event back to the client. This works with Silverlight and also with full .NET apps. This blog does not go into details on how to create a duplex channel but it does show the results of testing it behind a firewall and on a NATed connection.

Creating a Duplex Channel

To create a duplex channel you need to define a contract that says what you need the client to implement. On your main contract you need to include a reference to this interface . This interface is known as the callback contract. Your service interface will look like

[ServiceContract(SessionMode = SessionMode.Required,CallbackContract = typeof(ICallBackChannel))]
public interface IService1
{
[OperationContract(IsOneWay = true)] // IsOneWay means the caller will not expect a reply, this allows us to use call back channel without causing a deadlock.
void PingServer();
}
ICallBackChannel is the interface that defines the contract that the client will implement. Eg
public interface ICallBackChannel
{
[OperationContract(IsOneWay = true)]
void Ping();
}

Other ways to get events

You don't need to use duplex channels, you can manually implement the interface in the client and manage this yourself. You will find that this approach won't work when you are NATed or firewalled.

Testing it from a NATed connection (and firewalled)

If you use netTcpBinding then the client will keep the socket open and receive call-back messages. To test I used a Virtual PC and configured the connection to use a NAT. I turned the firewall on and tested it. It worked fine. If you were to use a wsDualHttpBinding behind a NAT it would not work. Let's examine the traffic that goes on with the http binding.
When I open the Client up, you see the following traffic on the server. (This has been captured using fiddler)





The client has managed to connect to the server but the server cannot talk back to the client. This is due to the address virtualxp-28481 not existing on our network as the client is NATed. So, Duplex Channels will not work if your client is NATed. A firewall may also block the port.
If we change the binding to use netTcpBinding then it works fine. We can see the open sockets on the client.




If we kill the connection the client channel goes into a faulted state.

Some other tricks

When the connection goes into a faulted state WCF raises an event. The client and the server can hook into this event: for example, the client could try to re-open the connection.
When I create the callback channel, we can attach to the Faulted event .
wisdomCallbackServer.InnerChannel.Faulted += new EventHandler(InnerChannel_Faulted);
The code that gets fired just reconnects.
Also, as a default your server will only allow 10 connections, this can be increased by changing the settings in the config file :



If I get time, I may post the test project on codeplex. I will re-blog if I do this.

Tuesday, December 1, 2009

Don't use .RenderBeginTag("pre")

Even in our modern XHTML/DHTML/AJAX world we still sometimes need to use a good old fashioned <pre>
tag. In our Records Management application, Wisdom, we needed to use one to display the contents of a plain text email that had been stored in Wisdom.  So I used the following code fragment:

writer.RenderBeginTag("pre");
writer.Write(HttpUtility.HtmlEncode(textContent));
writer.RenderEndTag();


but this didn't work very well! The first line of the email was indented as shown below:

         This is a
multi line
plain text email.

The reason is that RenderBeginTag uses some (normally helpful) logic internally to indent HTML tags and content so that it is more readable when you view the source. BUT you don't want this inside a <pre> tag because it preserves the whitespace! So you need to use WriteFullBeginTag and WriteEndTag methods on HtmlTextWriter instead:

writer.WriteFullBeginTag("pre");
writer.Write(HttpUtility.HtmlEncode(textContent));
writer.WriteEndTag("pre");

Friday, November 27, 2009

Reflection, byref and _COM

When using Refection against .NET codebyref just works as you would expect it to. When you are reflecting against COM byref does not work as you may expect.


If the target is .NET the following code would just work. The fact that we are creating the object from a class ID tells us it’s COM, so it won’t work.


var addin = GetCOMObject();

object[] objparams = new object[] { intVal,stringVal,etc};


var addintype = Type.GetTypeFromCLSID(new Guid("ADADAF30-E012-45db-95BE-E7544E918EBD")); // An Outlook Addin


var rval = addintype.InvokeMember(methodname, BindingFlags.InvokeMethod, null,addin, objparams);


Assume the method that we are invoking passes something back byref to param 2. The following code will not work show you the value :


stringval = (string) objparams[1];


To make this work with COM, you need to use ParameterModifers.


So, your code will look like


var addin = = GetCOMObject();

object[] objparams = new object[] { intVal,stringVal,etc};

ParameterModifier mods = new ParameterModifier(_params.Length);

mods[1] = true; // param 2 passes back by ref

ParameterModifier[] modstopass = { mods }; // we need an array
var addintype = Type.GetTypeFromCLSID(new Guid("ADADAF30-E012-45db-95BE-E7544E918EBD"));


var rval = addintype.InvokeMember(methodname, BindingFlags.InvokeMethod, null, addin, objparams, modstopass, null, null);


We had this issue with a C# Add-in for Outlook.


Let’s hope this “just works” with C#4 and the dynamic type. ..

Tuesday, November 24, 2009

Errors Enumerating DataRows in a DataTable in a strongly typed DataSet that has been upgraded from .NET 2.0 to .NET 3.5

In Visual Studio 2008 the version number of MSDataSetGenerator, the tool which generates strongly typed DataSets based upon a *.xsd file, increased from 2.0.50727.1433 to 2.0.50727.3074. The only significant difference in the output files is that when targeting framework .NET 3.5, the new version of the tool uses System.Data.TypedTableBase (from System.Data.DataSetExtensions) as the base class rather than System.Data.DataTable.
This sounds like a small difference but it can trip you up! Let’s say I have the following program:
class Program
{
   static void Main(string[] args)
   {
      SearchResultsDataSet ds = new SearchResultsDataSet();
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref1", "Title1");
      ds.SearchResults.AddSearchResultsRow(Guid.NewGuid(), "Ref2", "Title2");
      ds.AcceptChanges();
      foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults)
      {
         Console.WriteLine(row.Ref);
      }
      Console.ReadLine();
   }
}

SearchResultsDataSet is a strongly typed DataSet in a separate assembly called X. Assembly X and my test program are both built targeting .NET 2.0, and deployed to a customer site. I subsequently upgrade both my test program and X to .NET 3.5 and rebuild them, but only deploy the test program to the customer site; X has not changed, so I leave the existing copy in place. When my test program attempts to enumerate the DataRows in the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug> TypedDataSetTest.exe

Unhandled Exception: System.MissingMethodException: Method not found: 'System.Collections.IEnumerator SearchResultsDataTable.GetEnumerator()'.
at TypedDataSetTest.Program.Main(String[] args)

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

Or let’s say I do this the other way around: I upgrade both projects to .NET 3.5, rebuild both but deploy the new version of X alongside the old version of the test program. When I try to enumerate the “SearchResults” table I will get the following error:

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>
TypedDataSetTest.exe

Unhandled Exception: System.NullReferenceException: Object reference not set to
an instance of an object.
at System.Data.TypedTableBase`1.GetEnumerator()
at TypedDataSetTest.Program.Main(String[] args) in C:\Users\alexr\Documents\V
isual Studio 2008\Projects\TypedDataSetTest\Program.cs:line 17

C:\Users\alexr\Documents\Visual Studio 2008\Projects\TypedDataSetTest\bin\Debug>

What’s really going on AND what can make this problem difficult to spot
You may be thinking “What kind of fool would change the .NET framework an assembly was targeting and then not deploy it to the live environment”. And you’re right. But consider the following scenario:

  1. Create a strongly typed DataSet in an assembly that targets .NET 2.0.
  2. MSDataSetGenerator generates a .Designer.cs file containing the code.
  3. Upgrade the assembly to target .NET 3.5.
  4. The .Designer.cs file is already in place and it doesn’t get regenerated. It doesn’t get regenerated UNTIL you make a change to the *.xsd file.
This caught us out with our product, because we deploy Hotfixes by keeping the Assembly Version Number the same and deploying only assemblies which have been modified (using other file metadata to distinguish them). We were caught unawares by our .NET 3.5 strongly typed DataSets suddenly morphing from being based upon System.Data.DataTable to being based upon System.Data.TypedTableBase and some of our integration components broke.

Another thing that makes this problem hard to identify is that a lot of us (quite rightly) try to diagnose problems like this by using Reflector to decompile the assembly. In this case though, the source code of the test program is the same in both cases; it’s the underlying IL that is different. The convenient C# construct of foreach (SearchResultsDataSet.SearchResultsRow row in ds.SearchResults) has this going on behind the scenes:

First, for the .NET 2.0 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.IEnumerator [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_006d
L_0053: ldloc.2
L_0054: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
L_0059: castclass [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsRow
L_005e: stloc.1

And then the .NET 3.5 version:

L_0046: callvirt instance class [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet/SearchResultsDataTable [Wisdom.Common.DataSets]Diagonal.Wisdom.Common.DataSets.SearchResultsDataSet::get_SearchResults()
L_004b: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 [System.Data.DataSetExtensions]System.Data.TypedTableBase`1::GetEnumerator()
L_0050: stloc.2
L_0051: br.s L_0068
L_0053: ldloc.2
L_0054: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current()


(I might have missed a bit off the start and the end, I don’t understand IL that well). As you can see, in the second example the compiled assembly uses a generic enumerator rather than a standard System.Collections.IEnumerator. But this difference is not visible in C#.

Friday, November 20, 2009

Solving the Double Hop Issue

With a Batch File

I have come across many blogs and websites that go into great detail on how to solve the double hop issue. I have shared these with my colleagues and customers they all think that they seem too complex, and are therefore do not want to use Kerberos.

Solving the Double Hop Issue with Kerberos is not complex.
This diagram illustrates the problem.



This is taken from a very good blog on the same subject which goes into way more detail…

What you need to fix it

  • I would recommend running services as a DOMAIN USERs rather than network service or local users.
  • (“services” meaning Application Pools for web sites and SQL services)
  • Service Principal Names.
  • Local Machine Rights granted to the Service Accounts.
    • Act as part of operating system
    • Impersonate user
  • Settings in AD for the Machines to allow delegation.
  • Settings in AD for the Service Accounts to allow delegation.

 

What does this mean

A Service Principal Name (SPN) is the name by which a client uniquely identifies an instance of a service. For example HTTP/Servername is the SPN for a web site at Servername. MSSQLSvc/ServerName:1234 is the SPN for a SQL Service running on Servername on Port 1234 (see http://msdn.microsoft.com/en-us/library/ms677949(VS.85).aspx for more information on service principal names).

Local Machine rights are managed with Local Security Policy (found under Administrative Tools) .

Delegation is to pass someone’s user credentials to another process or server.

Both AD settings are edited with the MMC Snap-in “Active Directory Users and Computers”. You need to allow both the service users and the machine to delegate.

Some tools from the web


You need :
  • Found on the windows resource kit
    • NTRIGHTS.EXE (Set local machine rights)
    • SETSPN.exe
  • Installed with IIS
    • adsutil.vbs
  • On codeplex
    • machinedelegation.vbs
    • userdelegation.vbs
  • Below and on codeplex
    • Enable.bat

The Batch File


@REM ==================================
@REM Setup VARS
@REM ==================================

set MACHINE1=DBLHOP
set MACHINE2=DBLHOPSVR

set FQDN=domain.local

set USERACCOUNT=DOMAIN\ServiceAccount
@REM WEBID is the WEBSITE ID, THE Default website is 1
set WEBID=1

@REM ==================================
@REM Use adsutil to config IIS to use kerberos
@REM ==================================

cscript C:\inetput\adminscripts\adsutil.vbs set w3svc/WebSite/root/NTAuthenticationProviders Negotiate

@REM ==================================
@REM Use SETSPN to create the SPNs
@REM ==================================

setspn -A HTTP/%MACHINE1% %USERACCOUNT%
setspn -A HTTP/%MACHINE1%.%FQDN% %USERACCOUNT%

setspn -A HTTP/%MACHINE2% %USERACCOUNT%
setspn -A HTTP/%MACHINE2%.%FQDN% %USERACCOUNT%

@REM ==================================
@REM Use NTRIGHTS to enable Act as part of operating system and
@REM Impersonate a client after authentication
@REM ==================================

ntrights +r SeTcbPrivilege -u %USERACCOUNT% -m \\%MACHINE1%
ntrights +r SeTcbPrivilege -u %USERACCOUNT% -m \\%MACHINE2%

ntrights +r SeImpersonatePrivilege -u %USERACCOUNT% -m \\%MACHINE1%
ntrights +r SeImpersonatePrivilege -u %USERACCOUNT% -m \\%MACHINE2%


@REM ==================================
@REM Use VB Scripts to give the user the delegate right
@REM ==================================

cscript userdelegation.vbs %USERACCOUNT% enable

@REM ==================================
@REM Use VB Scripts to give the machine the delegate right
@REM ==================================

cscript machinedelegation.vbs %MACHINE1%
cscript machinedelegation.vbs %MACHINE2%

@REM ==================================
@REM Use IISRESET to restart IIS on both servers
@REM ==================================

iisreset %MACHINE1%
iisreset %MACHINE2%

Really good external websites

blog foo
ASP.NET Applicaiton to help

More Info

Diagonal

Monday, November 9, 2009

Wisdom and Microsoft Dynamics CRM

The wisdom team have been busy integrating Wisdom into Microsoft Dynamics CRM. Cases created in CRM can now be linked to a Wisdom case area. This gives Dynamics CRM Document and Records management capabilities and much more.

We believe that we have taken an interesting approach to this problem that will allow CRM administrators to link any CRM entity type to Wisdom thus giving any CRM entity EDRM capabilities.
Useful links

Microsoft Dynamics http://www.microsoft.com/dynamics/
Wisdom http://www.diagonal-consulting.com/Technology/Wisdom/Pages/Default.aspx