Monday 30 June 2014

Configure Federation Trust for Claim Based authentication in Ms Dynamics CRM 2013 using ADFS 3.0 ( Windows Server 2012 R2) part 3

In part 1, I described how to install and configure ADFS on a Windows 2012 R2 server. In part 2, I described how to configure Ms Dynamics CRM 2013 to use claim based authentication and in this final post of this series I configure a federation trust to allow users from a separate forest to access MS Dynamics CRM 2013.
 
In our case, this is due to an acquisition, so that we are hosting Dynamics CRM in a domain called dev.local and the company bought has their own domain called taleb.local.
 
The objective is to allow users from taleb.local to log on, with their taleb.local accounts into our instance of Dynamics CRM, so we could say that taleb.local is the Accounts domain and dev.local is the Dynamics Domain.
 
Pre-Requisites:
  • Working ADFS servers on both domain/forests
  • Name resolution working between domain/forests as well as inside forests, I have used stub zones but any way that works for you is good.
  • TCP port 443 between ADFS servers and Dynamics Servers(or at least Front End Servers) open.
  • Account with permissions to configure both ADFS servers.
 
In our case, due to the internal use, Public certificates are not in use, so the first step is to ensure that taleb.local machines trust the dev.local CA. This is out of scope for this post, see this link for details.
 
The ADFS server(s) in dev.local need to trust the taleb.local CA, which can be achieved by adding the certificate to the trusted store for the computer account.
 
On the Accounts domain's (taleb.local) ADFS server, add a Relying Party Trust for the dev.local ADFS endpoint:
 







When the Claim Rules window opens, Click add Rule and Add a Send LDAP Attribute as Claims Claim Rule. The only LDAP Attribute that we are after is UPN.






On the Dynamics domain's (dev.local) ADFS Server, Add a Claims Provider Trust for the Accounts domain (taleb.local) ADFS endpoint:








When the Claim Rules window opens, Click add Rule and Add a Pass Through or Filter an Incoming Claim Claim Rule. I think that we only really need UPN, but I've added Windows Account Name and Primary SID as well for good measure.





 
 
At this point, all that remains is for accounts from the Accounts domain (taleb.local) to be added to Dynamics CRM as users and given a role so that they can log in to Dynamics CRM.
 
Ensure that UPN is used to add a new user to Dynamics CRM, e.g. For a user with logon: taleb\NN, the username in Dynamics CRM would be: nn@taleb.local. Annoyingly, it doesn't retrieve first name, last name so these will need to be added manually. I'd love to know if it's actually possible (to get the first/last name to auto populate) by adding more claims but I've never been able to get it working.
 
I've added both domains to the trusted sites as our default configuration is for automatic logon with current username and password on trusted sites.
 

Navigating to https://allinone.dev.local/lospolloshermanos/main.aspx from a machine in the accounts domain (taleb.local) brings this screen up: 


Selecting sts1.taleb.local will redirect to https://allinone.dev.local/lospolloshermanos/main.aspx with the current account from the accounts domain (taleb.local).



 

Monday 23 June 2014

Use Linq Joins (Method Syntax) with Late Binding in Ms Dynamics CRM 2011/2013

There seems to be no examples of using Linq with late binding and method syntax about, until now:

var collection = context.CreateQuery("service")
.Join(context.CreateQuery("e2_dictionary"),x => x["serviceid"], y => (y["e2_serviceid"as EntityReference).Id, (x, y) => y)
.Where(x => x["name"].Equals(serviceName));
The out of the box service entity is pretty locked down, it's not possible to add N:1 or N:N relationships, so in order to implement a classification system for the services, I created the e2_dictionary entity.

e2_dictionary has a N:1 with e2_type and service, which means that there is some validation needed in a plug-in to ensure that when that nobody creates more than one e2_dictionary and this is why the snippet above, serviceName is the name of the service :)
 
 


Monday 16 June 2014

Install/Uninstall library/assembly (DLL) in to GAC using PowerShell

On Friday, I screwed a little bit, as the build relied on some assemblies being in the GAC and I had not added them to the GAC.
 
I normally use Gacutil but this being a test server, it had no such tools, but no matter, PowerShell to the rescue.
Add-Type -AssemblyName System.EnterpriseServices
$Publisher = New-Object System.EnterpriseServices.Internal.Publish
$Publisher.GacInstall("C:\Users\Account\Desktop\mydll.dll)
gwmi win32_service | ? {$_.Name -match "MSCRM|W3SVC"} | %{Restart-Service -Name $_.Name}

The last step restarts all services containing MSCRM on its name as well as the World Wide Publishing Service, so that both async and sync plugins can make use of this library.

Removing the Assembly from the Gac can be achieved with the following commands:
Add-Type -AssemblyName System.EnterpriseServices
$Publisher = New-Object System.EnterpriseServices.Internal.Publish
$Publisher.GacRemove("C:\Users\Account\Desktop\mydll.dll)
gwmi win32_service | ? {$_.Name -match "MSCRM|W3SVC"} | %{Restart-Service -Name $_.Name}
Add-Type is a PowerShell 3.0 onwards cmdlet, you can use this instead:
[System.Reflection.Assembly]::LoadWithPartialName("System.EnterpriseServices")

Wednesday 11 June 2014

TIL - Build solution with msbuild using multiple reference paths

More Jenkins issues today with the build, in particular using multiple reference paths as we finally added some much needed trace logging to the build, so I changed the build command to this:
msbuild solution.sln /p:Configuration=RELEASE /t:Clean;Build /p:referencepath="C:\ReferencePath\Sharepoint;C:\ReferencePath\Log4Net"
Incidentally, if you want to run msbuild from PowerShell, the quotes need escaping:
msbuild solution.sln /p:Configuration=RELEASE /t:"Clean;Build" /p:referencepath=`"C:\ReferencePath\Sharepoint;C:\ReferencePath\Log4Net`"

Tuesday 10 June 2014

TIL - Clean a build from MSBuild

Version numbers were wrong on the build as Jenkins was not cleaning the build before compiling so I changed the build command to this:
msbuild solution.sln /p:Configuration=RELEASE /t:Clean;Build

Incidentally, if you want to run msbuild from PowerShell, the build targets need protecting from the shell like this:
msbuild .\solution.sln /p:Configuration=RELEASE /t:"Clean;Build"

Monday 2 June 2014

Create OptionSet in MS Dynamics CRM 2011/2013 programmatically

I was asked last week to create a new entity with a few drop down menus, our analyst is still not quite with the Dynamics CRM lingo :).

There was one for Title or Salutation, which contained just about every title you could possibly imagine, from military ranks to various religious positions to academia, you name it, it was there. Similarly, there was another one for ethnicity, which I pointed out customers are loath to divulge, but no matter we needed that as well with its myriad categories, all in all I was looking at upwards of 100 options in total across 5 option sets so I thought that this might be a good time to look up how to create a global option set programmatically.

At any rate, here's the code:


/// <summary>
/// Creates Global OptionSet against Solution.
/// </summary>
/// <param name="service"/>Crm Service</param>
/// <param name="filePath"/>Path to a file that contains the optionset labels, one per line</param>
/// <param name="optionSetName"/>Unique Name of OptionSet</param>
/// <param name="displayName"/>Display Name of OptionSet</param>
/// <param name="solution"/>Unique Name of Solution OptionSet will be created against</param>
/// <param name="initialValue"/>Solution's OptionSet number prefix</param>
private void CreateOptionSet(IOrganizationService service, string filePath, string optionSetName, string displayName, string solution, int initialValue)
{
    try
    {
        int languageCode = 1033;

        OptionMetadataCollection options = new OptionMetadataCollection();

        using (StreamReader reader = new StreamReader(filePath))
        {
            while (!reader.EndOfStream)
            {
                options.Add(new OptionMetadata(new Label(reader.ReadLine(), languageCode), initialValue));
                initialValue++;
            }
        }

        OptionSetMetadata optionSet = new OptionSetMetadata();
        optionSet.Name = optionSetName;
        optionSet.DisplayName = new Label(displayName, 1033);
        optionSet.IsGlobal = true;
        optionSet.OptionSetType = OptionSetType.Picklist;

        CreateOptionSetRequest request = new CreateOptionSetRequest();
        request.OptionSet = optionSet;
        request.SolutionUniqueName = solution;

        service.Execute(request);
       
        foreach (var item in options)
        {
            InsertOptionValueRequest insertRequest = new InsertOptionValueRequest();
            insertRequest.OptionSetName = optionSetName;
            insertRequest.Label = item.Label;
            insertRequest.Value = item.Value;

            service.Execute(insertRequest);

            Console.WriteLine("Added OptionSet Item {0} with Value {1}", item.Label, item.Value); 
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("An error occurred creating OptionSet {0}. Exception: {1}", optionSetName, ex);
    }
}