Extending Active Directory Schema to Store Application Configuration (+PowerShell Examples)

These days I work together with many top-notch developers, dealing with very different projects, solutions and applications (not only with SCOM management packs :)). And what I like most of all about being in this very diverse community is the variety of questions whose folks bring onto the table. Here is the most recent one:

Can we store configuration information for our application in AD? If yes, then how?

I was dealing a lot with the Exchange Server during last months, so for me the answer for the first question was obvious – yes, you can store the configuration information in the Active Directory. Finding the answer for the second question was tricky – I hadn’t enough knowledge about implementing this kind of stuff. So I made a dive into the theory and ended with a bunch of links and some samples written in PowerShell. Here it goes.

Disclaimer

All samples provided here are provided As Is. You may use them at your own risk.

Note, that any changes you apply to AD Schema are not reversible – consider testing any changes in the lab first.

All samples are written in PowerShell, so both IT Pros and developers can use them. (I do believe that professional developers can read the code in any language and are able to easily convert PowerShell samples into C#.)

Some theory

Many modern applications have multi-tier architecture. To be able to act as a whole, some application components might need to share configuration information with other ones. Exchange Server is a good example of such application – the typical deployment includes a number of servers, holding different roles (client access server, mailbox server, edge transport), distributed across entire organization.

The Active Directory suites that need in a very good way – everyone within organization can access the information when required and permitted, also AD infrastructure is usually highly available.

There are zillion ways to store the required information and it is up to software developers to decide how exactly they want to implement that. Here are some common points of consideration:

  • What information should be stored?
  • Who should be able to access the information?
  • Is that information sensitive?
  • Where do we need that information (anywhere in the forest, anywhere in the domain, selected Domain Controllers)?
  • Can we (do we want to) extend the schema?

It is impossible to discuss and illustrate all possible options in a one blog post, so let’s concentrate on just one example.


Here are the functional requirements:

  1. Application has 3 deployments: Development, QA and Production;
  2. Each deployment has a connection point, which is served by one application server at a time;
  3. Clients for development, QA and production deployments cannot be placed into the same Organizational Unit (so we have at least 3 OUs – one per deployment);
  4. In future we may want to introduce more deployments;
  5. In future we may want to have more granular OUs.
  6. The configuration should serve the entire forest.

And here is how we can translate that into implementation design:

  1. We need to store configuration information for our application in the “Configuration” Directory Partition (so it will be replicated across the forest);
  2. We want to store configuration details for each deployment separately;
  3. We do not want to have multiple copies of configuration information (i.e. we need one and only one object per deployment);
  4. We want to assign the deployment to the OU.

Extending Active Directory Schema

Extending Active Directory Schema is required only if there is no existing class and/or attribute which can fulfill your particular requirements. When making a decision, you should keep in mind that schema extension impacts entire domain forest in a number of ways.

In our case we want to be able to assign the deployment directly to the Organizational Unit, so we need either an object within the OU or some extra attribute. Object can be moved, attribute can’t. Thus we decide to extend the schema. (As I have already mentioned, there are many ways to implement the same thing, it is up to developer to decide).

Here is how this can be achieved:

function New-ADObjectID()
{
    # this code is from this blog post: http://fkazi.blogspot.ru/2013/04/creating-custom-active-directory_27.html
    $Prefix="1.2.840.113556.1.8000.2554" 
    $GUID=[System.Guid]::NewGuid().ToString() 
    $Parts=@() 
    $Parts+=[UInt64]::Parse($guid.SubString(0,4),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(4,4),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(9,4),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(14,4),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(19,4),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(24,6),"AllowHexSpecifier") 
    $Parts+=[UInt64]::Parse($guid.SubString(30,6),"AllowHexSpecifier") 
    $OID=[String]::Format("{0}.{1}.{2}.{3}.{4}.{5}.{6}.{7}",$prefix,$Parts[0],$Parts[1],$Parts[2],$Parts[3],$Parts[4],$Parts[5],$Parts[6]) 
    return $oid 
}

# we need System.DirectoryServices .NET assembly to manipulate AD attrinutes, classes and properties
[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices")

# Find AD Context - current forrest
$adContext = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext -ArgumentList @([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest)

# Get Schema
[System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]$adSchema = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema()

if($adSchema -ne $null)
{
    "We have AD schema: {0}" -f $adSchema.Name

    # Find OU schema class
    $ouClass = $adSchema.FindClass("organizationalUnit")
    "OU Class found: {0}" -f $ouClass.Name
    "OU Class optional properties"

    # Get schema class optional properties - we need them to check if the property already exists
    $ouClassOptionalProperties = $ouClass.OptionalProperties

    # this is the name of attribute we'll use to store the name of a connection point
    $propertyName = "OksbApplicationInstance"

    # Get the property (we'll get $null if there is no such property)
    $ouClassProp = $ouClassOptionalProperties | ?{$_.Name -eq $propertyName}
   
    if($ouClassProp -ne $null)
    {
        "Property already exists:"
        $ouClassProp 
    }
    else
    {
        "Adding property ({0}) to class ({1})" -f $propertyName, $ouClass.Name

        # let's extend the schema

        # 1. Try to find the attribute first
        [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty] $adProperty = $null
        
        try 
        {
            $adProperty = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty]::FindByName($adContext, $propertyName)
        }
        catch [Exception]
        {
            Write-Host "Attribute not found: $($_.Exception.GetType().FullName)" -ForegroundColor Magenta
            $adProperty = $null
        }

        # 2. define new property if not found
        if($adProperty -ne $null)
        {
            "Attribute {0} found" -f $propertyName
        }
        else
        {
            "Creating attribute {0}" -f $propertyName
            $adProperty = New-Object System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaProperty -ArgumentList @($adContext, $propertyName)
            $adProperty.CommonName = $propertyName
            $adProperty.IsSingleValued = $true
            $adProperty.Oid = New-ADObjectID
            $adProperty.Syntax = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySyntax]::CaseIgnoreString
            $adProperty.Save() # !!! this is not reversable !!!
        }

        # 3. add property to the class
        $adSchemaPropertyIndex = $ouClassOptionalProperties.Add($adProperty)

        "Property added with Index = {0}" -f $adSchemaPropertyIndex


        # 4. commit changes - !!! this is not reversable !!!
        [bool]$isOuSaved = $true
        try
        {
            $ouClass.Save() # !!! this is not reversable !!!
        }
        catch [Exception]
        {
            Write-Host "OU class wasn't saved: $($_.Exception.GetType().FullName)" -ForegroundColor Magenta
            $isOuSaved = $false
        }

        if($isOuSaved) { "OU Class Saved!" }
    }
    

}
else
{
    "Cannot load schema"
}

Adding objects to store Application Configuration

To store configuration information we use:
a) Container object to have all our configurations in one place
and
b) serviceConnectionPoint objects to store our configuration information.

# add service container to store connection points
New-ADObject -Name 'OKSB Application Configuration' -Type Container `
    -Path "CN=Services,CN=Configuration,DC=ok01,DC=local"

# add service connection points
New-ADObject -Name 'OksbProdConfig001' -Type serviceConnectionPoint `
    -OtherAttributes @{'serviceBindingInformation'="prodsrv.ok01.local"} `
    -Path "CN=OKSB Application Configuration,CN=Services,CN=Configuration,DC=ok01,DC=local"
New-ADObject -Name 'OksbQaConfig001' -Type serviceConnectionPoint `
    -OtherAttributes @{'serviceBindingInformation'="qasrv.ok01.local"} `
    -Path "CN=OKSB Application Configuration,CN=Services,CN=Configuration,DC=ok01,DC=local"
New-ADObject -Name 'OksbDevConfig001' -Type serviceConnectionPoint `
    -OtherAttributes @{'serviceBindingInformation'="devsrv.ok01.local"} `
    -Path "CN=OKSB Application Configuration,CN=Services,CN=Configuration,DC=ok01,DC=local"

Now, when we have our configuration details in place, we can set the property of Organizational Units to point to our configuration objects. Of course, any inconsistencies like missing configuration or unavailable service should be properly handled by the application itself.

# set custom attribute for OUs to store the name of the connection point
Set-ADObject -Identity "OU=OksbProduction,OU=OksbApplicationServers,DC=ok01,DC=local" `
    -Add @{OksbApplicationInstance="OksbProdConfig001"}
Set-ADObject -Identity "OU=OksbQA,OU=OksbApplicationServers,DC=ok01,DC=local" `
    -Add @{OksbApplicationInstance="OksbQAConfig001"}
Set-ADObject -Identity "OU=OksbDev,OU=OksbApplicationServers,DC=ok01,DC=local" `
    -Add @{OksbApplicationInstance="OksbDevConfig001"}

Useful links

Leave a Comment