October 10, 2010

Powershell: Add Active Directory connection with User Profiles

User Profiles now (SharePoint 2010) is a SharePoint service application that use a Microsoft Forefront Identity 2010 as a underlying tool for SharePoint profiles synchronization.

This is a worst change in SharePoint from previous version. Now, It’s very slow, very hard-configurable and causes huge amount of problems during configure procedures and further work. To verify this you can read these guides: Technet: Configure profile synchronization, Technet: Maintain profile synchronization.

Quote: Incorrect permissions are the most common cause of errors in configuring profile synchronization

Quote: Farm Account must Be a member of the Administrators group on the synchronization server (see the Profile Synchronization Planning worksheet). You can remove this permission after you have configured the User Profile Synchronization service.

Wonderful, isn’t it?

And now We’ll talk about setting up an Active Directory connection with user profiles. You can configure such connection with administrative pages of User Profile Service Application, but these pages are very slow and buggy.

Note: For example, if Active Directory has some granular security settings you may get a timeout error as described here. You can configure timeouts with powershell (here, last paragraph), but that doesn’t help in this case.

We must choose another way: let’s automate the creation of connections with great help of PowerShell gods.

First of all, the core of our script is the UserProfileConfigManager class that contains the ConnectionManager property with several methods for adding new connections with user profiles.

Screenshot: ConnectionManager class

We are interested in the AddActiveDirectoryConnection method, of course. This is a very “simple” method with 8 parameters (!) .

public DirectoryServiceConnection AddActiveDirectoryConnection(
ConnectionType type,
string
displayName,
string
server,
bool
useSSL,
string
accountDomain,
string
accountUsername,
SecureString accountPassword,
List<DirectoryServiceNamingContext> namingContexts,
string
spsClaimProviderTypeValue,
string
spsClaimProviderIdValue
)

And here there is another type DirectoryServiceNamingContext which has importance for us.


Screenshot: DirectoryServiceNamingContext class


Its constructors have large signatures too – 8 and 9 parameters:

public DirectoryServiceNamingContext(
string
distinguishedName,
string
domainName,
bool
isDomain,
Guid objectId,
List<
string
> containersIncluded,
List<
string
> containersExcluded,
List<
string
> preferredDomainControllers,
bool
useOnlyPreferredDomainControllers
)


Thanks God, we have intuitive parameter names and types. So, let’s code our script:


Add-SPActiveDirectoryConnection.ps1:


Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

function Get-SPServiceContext
(
   
[Microsoft.SharePoint.Administration.SPServiceApplication]$profileApp
)
{
   
if($profileApp -eq $null
)
    {
       
# Get first User Profile Service Application
        $profileApp = @(Get-SPServiceApplication |
 
           
? { $_.TypeName -eq "User Profile Service Application" })[0]
    }
   
   
return [Microsoft.SharePoint.SPServiceContext]::
GetContext(
       
$profileApp.ServiceApplicationProxyGroup,
 
       
[Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::
Default)
}


function Convert-ToList($inputObject, [System.String]$Type
)
{
   
begin
    {
       
if($type -eq $null -or $type -eq ''
) 
        {
           
$type = [string]
        }
   
       
$list = New-Object System.Collections.Generic.List[$type]
    }
   
   
process { $list.Add($_
) }
   
   
end
    {
       
return ,$list
    }
}


function Get-DC($domainName
)
{
   
return ("DC=" + $domainName.Replace(".", ",DC="
))
}


# Types

$DirectoryServiceNamingContextType = [Microsoft.Office.Server.UserProfiles.DirectoryServiceNamingContext, Microsoft.Office.Server.UserProfiles, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c]

# Globals

$connectionName = "example.com"
$domainName = "example.com"
$accountName = "EXAMPLE\User"
$password = ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force

$partitions =
 @{
    
"example.com" = @("DC=example,DC=com"
);
     };


# Main()

# Prepare Parameters


$userDomain = $accountName.Substring(0, $accountName.IndexOf("\"))
$userName = $accountName.Substring($accountName.IndexOf("\") + 1)

$dnContexts = $partitions.GetEnumerator() |
     %
 {
    
$domainName = $_.
Key
    
$containers = $_.Value | Convert-ToList
   
    
$partition = [ADSI]("LDAP://" + (Get-DC $domainName
))

    
$partitionId = New-Object Guid($partition.
objectGUID)

    
New-Object $DirectoryServiceNamingContextType
(
       
$partition.distinguishedName,
 
       
$domainName,
 
       
<# isDomain: #> $false,
 
       
<# objectId: #> $partitionId,
 
       
<# containersIncluded: #> $containers,
 
       
<# containersExcluded: #> $null,
 
       
<# preferredDomainControllers: #> $null,
 
       
<# useOnlyPreferredDomainControllers: #> $false
)
    } 
| Convert-ToList -Type 
$DirectoryServiceNamingContextType

$partition
 = [ADSI]("LDAP://CN=Configuration," + (Get-DC $domainName))
$partitionId = New-Object Guid($partition.objectGUID)

$containers = @($partition.distinguishedName) | Convert-ToList

$dnContext = New-Object $DirectoryServiceNamingContextType
(
   
$partition.distinguishedName,
 
   
$domainName,
 
   
<# isDomain: #> $true,
 
   
<# objectId: #> $partitionId,
 
   
<# containersIncluded: #> $containers,
 
   
<# containersExcluded: #> $null,
 
   
<# preferredDomainControllers: #> $null,
 
   
<# useOnlyPreferredDomainControllers: #> $false)

$dnContexts.Add($dnContext)


# Create Active Directory Connection

$serviceContext = Get-SPServiceContext

$configManager = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager($serviceContext)

if($configManager.ConnectionManager.Contains($connectionName) -eq $false
)
{
   
$configManager.ConnectionManager.
AddActiveDirectoryConnection(
       
[Microsoft.Office.Server.UserProfiles.ConnectionType]::ActiveDirectory,
 
       
$connectionName, $domainName, <# useSSL: #> $false,
 
       
$userDomain, $userName, $password,
 
       
<# namingContexts #> $dnContexts,
 
       
<# spsClaimProviderTypeValue: #> $null,
 
       
<# spsClaimProviderIdValue: #> $null
)
}

else
{
   
Write-Host "Connection '$connectionName' already exist. Delete it before run this script."
}

That’s All.


And, of course, I hate it!

25 comments:

  1. Awesome stuff.

    How do we set the context for specific domain t import.. when i ran the script.. i see the lis of domains.. but if i wanted to selectthe context to specific domain.. how do i do it?

    ReplyDelete
  2. If you have a domain forest, and/or you need to select some organization units than you need to set partitions via the $partitions variable (from Globals section of the script above).
    For example:

    $partitions = @{
    "example.com" = @("OU=Users,DC=example,DC=com", "OU=Guests,DC=example,DC=com");
    "north.example.com" = @("OU=Users,DC=north,DC=example,DC=com", "OU=External,DC=north,DC=example,DC=com");
    };

    For this case we select two organization units in the root domain "example.com" and two OU's in the subdomain north.example.com.

    ReplyDelete
    Replies
    1. Hi, great post. I need to select only a couple of OU's from the domain, but the script still selects every OU in the containers section. My code is below, have I missed something?

      $partitions = @{
      "adslocal.net" = @("OU=Admin Users,OU=Windows 7,OU=Users,OU=London,DC=contoso,DC=com","OU=Admin Users,OU=Windows 7,OU=Users,OU=Munich,DC=contoso,DC=com");
      };

      Delete
  3. Excellent !!!

    BTW, Before i try, By doing this will it reflect in UI while populating containers?
    I mean UI will reflect the context selected?

    ReplyDelete
  4. Yes, of course. This is a reimplementation of the standard mechanism which used by the UI-page (EditDSServer.aspx). In the UI you will see (when you click "Populate Containers") selected containers as expected.

    ReplyDelete
  5. Excellent Andrew, It worked for me..

    Just to understand more on the code. What exactly are we doing in the script for following lines

    $partition = [ADSI]("LDAP://CN=Configuration," + (Get-DC $domainName))
    $partitionId = New-Object Guid($partition.objectGUID)

    ReplyDelete
  6. You are welcome, Mandy.

    This code adds the special configuration partition from an active directory which contains the domain forest's configuration and the active directory's scheme.
    http://technet.microsoft.com/en-us/library/cc772886%28WS.10%29.aspx

    ReplyDelete
  7. Excelent! Just not fully working yet... I have everything setup in the script as far a params go, but i get this error: Exception calling "AddActiveDirectoryConnection" with "10" argument(s): "Unable
    to process Create message"
    At F:\scripts\CreateUPSConnections.ps1:104 char:66
    + $configManager.ConnectionManager.AddActiveDirectoryConnection <<<< (
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    ReplyDelete
  8. Hi there, I'm also getting the AddActiveDirectoryConnection" with "10" argument

    Error - did you find a way to resolve this?

    ReplyDelete
  9. Hello, All!

    If you get some strange exception You must check you permissions on User Profile Service Application or just run this script with Server Farm Account.

    With best regards, Andrew MossHater.

    ReplyDelete
  10. Thanks for putting this script together, it's exactly what I'm looking for. I've integrated into my larger install and config script but I keep hitting a problem. This:
    return Microsoft.SharePoint.SPServiceContext]::GetContext($profileApp.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default)

    Always comes back with a 0000 SiteSubscriptionID. It's part of Get-SPServiceContext and gets run without parameters. Everything seems to go fine until it hits this return statement. Any ideas?

    ReplyDelete
  11. Nice script, but I receive an error:
    New-Object : Exception calling ".ctor" with "1" argument(s): "The located assembly's manifest definition does not match
    the assembly reference. (Exception from HRESULT: 0x80131040)"
    At D:\Add-SPActiveDirectoryConnection.ps1:102 char:28
    + $configManager = New-Object <<<< Microsoft.Office.Server.UserProfiles.UserProfileConfigManager($serviceContext)
    + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

    ReplyDelete
  12. Hi Andrew,

    I also got the expection when AddActiveDirectoryConnection - but I traced it ;-)

    The problem occurs when you have a setup with subdomains - then you do not correctly retrieve the Configuration container :-)

    To fix it, change the following lines under #Global:
    $connectionName = "example.com"
    $forestName = "example.com"
    $accountName = "EXAMPLE\SyncUser"
    $passclear = 'P@ssW0rd'
    $password = ConvertTo-SecureString $passclear -AsPlainText -Force

    and then, further down, instead of
    $partition = [ADSI]("LDAP://" + (Get-DC $domainName))
    use
    $partition = new-object DirectoryServices.DirectoryEntry("LDAP://" + (Get-DC $domainName), $accountName, $passclear)
    to bind as the User that is used for the sync (your admin/farm account may not have access to the OU)

    Last but not least, when creating the configuration context, use the following line
    $partition = new-object DirectoryServices.DirectoryEntry"LDAP://CN=Configuration," + (Get-DC $forestName), $accountName, $passclear)
    instead of
    $partition = [ADSI]("LDAP://CN=Configuration," + (Get-DC $domainName))

    That way, domain-name will not be overwritten with some perhaps invalid value and you use the credentials of the sync user (which should have access to the configuration container as per MS-spec.

    Hope that helps someon, it fixed the problem for me!

    ReplyDelete
  13. Really cool stuff.
    However, I'm wondering:
    Does the management agent run profiles (DS_EXPORT, DS_FULLSYNC, etc) are created by this method? No ones are created on my side and I found no information on internet.
    Do I miss something or creating the connection in the wrong way ?

    Thank you

    ReplyDelete
  14. Hello,

    Really cool stuff. Howerver, one question
    Does the call to AddActiveDirectoryConnection create the management agent run profiles such as DS_FULLSYNC, DS_FULLIMPORT, DS_DELTASYNC ?

    These ones are automatically created when creating the active directory connection from sharepoint central admin, but no ones show up when trying to create it programmatically.

    Regards

    ReplyDelete
  15. Weird... I got a script IDENTICAL to this one from PSS back in october. Did you create this script yourself?

    ReplyDelete
  16. Get-SPServiceContext is returning a SiteSubcriptionId of 00000000-0000-0000-0000-000000000000?

    Then receiving "object reference not set to"... error on a the next method. Any ideas what may be incorrect?

    Thanks

    New-Object : Exception calling ".ctor" with "0" argument(s): "Value cannot be null.
    Parameter name: serviceContext"
    At C:\Users\Public\Documents\Adduserprofileconnection.ps1:107 char:28
    + $configManager = New-Object <<<< Microsoft.Office.Server.UserProfiles.UserProfileConfigManager($serviceContext)
    + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

    ReplyDelete
  17. Using Farm Admin account yet getting the same erorr as mentioned by Tiffany... Like 114: char 66

    ReplyDelete
  18. Sometimes work fine. But sometimes not. If not exception looks like:
    New-Object : Exception calling ".ctor" with "1" argument(s): "The located assem
    bly's manifest definition does not match the assembly reference. (Exception fro
    m HRESULT: 0x80131040)"
    At line:1 char:32
    + $configManager = New-Object <<<< Microsoft.Office.Server.UserProfiles.Us
    erProfileConfigManager($serviceContext)
    + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvoca
    tionException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.Power
    Shell.Commands.NewObjectCommand
    But if i try to re-run powershell window script works well. That's not so good. i tried iisreset, reload sharepoint powershell snapin, reload next assemblies:
    Microsoft.SharePoint.Administration.SPServiceApplication
    Microsoft.SharePoint.SPSiteSubscriptionIdentifier
    Microsoft.SharePoint.SPServiceContext
    Microsoft.Office.Server.UserProfiles
    Microsoft.Office.Server.UserProfiles.UserProfileConfigManager
    and nothing helps me... May be you can show me the light in that dark hole :)

    ReplyDelete
  19. Hmm I am still receiving the error:

    Exception calling "AddActiveDirectoryConnection" with "10" argument(s): "Unable to process Create message"

    I am running the AddActiveDirectoryConnection call under my farm account.

    I have also double checked the farm accounts permissions on the User Profile Service and it has all permissions.

    Any more ideas?

    ReplyDelete
  20. Great scripts. I needed to use it because the UI was not able to expand one of the OU's on the main level.

    An addition to your script is to add the exclude partions. Probably the following list can be added as default.


    OU=Administrative,DC=contoso,DC=local
    CN=Computers,DC=contoso,DC=local
    OU=Domain Controllers,DC=contoso,DC=local
    CN=ForeignSecurityPrincipals,DC=contoso,DC=local
    CN=Managed Service Accounts,DC=contoso,DC=local
    CN=Program Data,DC=contoso,DC=local
    CN=System,DC=contoso,DC=local
    CN=Users,DC=contoso,DC=local
    CN=Builtin,DC=contoso,DC=local
    CN=Infrastructure,DC=contoso,DC=local
    CN=LostAndFound,DC=contoso,DC=local
    CN=NTDS Quotas,DC=contoso,DC=local
    DC=DomainDnsZones,DC=contoso,DC=local
    DC=ForestDnsZones,DC=contoso,DC=local
    CN=Configuration,DC=contoso,DC=local

    Kind regards,
    Joran Markx

    ReplyDelete
  21. Hello Andrew,

    I need to add many different OU's to be configured in one single connection name. Is it possible to do this using ur script?

    Thanks,
    Raja

    ReplyDelete
  22. Cheers for the post. I feel your hate! :)
    We have very specific OUs we need to setup and and synch, however, no matter what I enter in the partitions globals, the connection gui shows ALL domain containers as being selected. I have change the timeouts just in case ...

    ReplyDelete
  23. Hi Great Post , I'm also getting the AddActiveDirectoryConnection" with "10" argument error has anyone managed to resolve this I have used the Farm Admin account to run the script

    ReplyDelete
  24. Hi, a really good post. I need to get my script to only select certain ou's and not every object in the domain like it is doing right now. I have set the $partitions to select only two OU's like below but it still selects every object from every OU. Any suggestions?

    $partitions = @{
    "example.com" = @("OU=Users,DC=example,DC=com", "OU=Guests,DC=example,DC=com");

    ReplyDelete