June 17th, 2015

PowerShell and the Active Directory Schema: Part 2

Doctor Scripto
Scripter

Summary: Guest blogger, Andy Schneider, continues his discussion about extending the Active Directory schema.

Microsoft Scripting Guy, Ed Wilson, is here. Welcome back guest blogger, Andy Schneider, for Part 2 of his series. If you missed yesterday's post, see PowerShell and the Active Directory Schema: Part 1.

Yesterday, we looked at what the Active Directory schema is and how to access details of the schema by using Windows PowerShell. In this post, we are going to look at how we can look at the schema, and also update the schema. If you didn’t get a chance to read Extending the Active Directory Schema by Brian Desmond, I highly suggest you read through it to gain an understanding of the ramifications of schema extensions and how to properly plan for them.

Yesterday, I left you with the following lines of code which would give us all kinds of information about the schema objects we have in Active Directory:

$schemaPath = (Get-ADRootDSE).schemaNamingContext

Get-ADObject -filter * -SearchBase $schemaPath -Properties * | where Name -like "user"

Image of command output

The question now is, “Which ones do we care about if we want to add our own schema extensions to the user class?”

The way I went about this was to start looking at how schema extensions are typically done. More often than not, extensions are implemented using the LDAP Data Interchange Format, also known as LDIF. There is a tool in Windows called LDIFDE.exe that you can use to import and export data. LDIFDE.exe can also be used to add attributes and classes to the Active Directory schema.

In addition to examining LDIF files, I also went through the manual process of using the schema management tools to see what kinds of input go into the GUI. Following is a screenshot of that menu.

   Note You have to register a DLL to be able to get the Schema Management MMC. You can do this with the
   command regsvr32 schmmgmt.dll.

Image of menu

By looking through a variety of LDIF file examples and the GUI tool, we can figure out what we need to supply when we create new attributes. Here is a short list:

Attribute

Description

Name

Name of the attribute

lDAPDisplayName

LDAP display name

attributeID

Unique Object Identifier, known as an OID

oMSyntax

Determines type of data stored in the attribute (int, string, bool). 
For reference, see Syntaxes.

isSingleValued

True or False. Attribute may have more than one value
(for example, proxyAddresses)

adminDescription

Description of the attribute

The one slightly tricky attribute that we need to worry about is attributeID. This needs to be a unique OID. For production, you really should register for a Private Enterprise Number with IANA. You can do this here.

However, for development purposes, we can use the following code to generate an OID:

Function New-AttributeID {

    $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])

        $oid

}

Image of command output

For this demonstration, we are going to add a new attribute to the user class called asFavoriteColor. I am using the “as” prefix simply to identify this attribute. You can use your company’s initials or some other way to distinguish your custom extensions from other attributes. To create a new attribute, we are going to use the New-ADObject cmdlet.

The path is going to be in the schemaNamingContext container, which we already have. We have also seen that schema attribute objects are of the type attributeSchema. New schema class objects are of the type classSchema.

The only thing that remains are some of these properties that are unique to schema objects. We will need to store these in a hash table and pass them in via the –OtherAttributes parameter:

$schemaPath = (Get-ADRootDSE).schemaNamingContext

$oid = New-AttributeID

$attributes = @{

      lDAPDisplayName = 'asFavoriteColor';

      attributeId = $oid;

      oMSyntax = 20;

      attributeSyntax = "2.5.5.4";

      isSingleValued = $true;

      adminDescription = 'favorite colors are really important';

      searchflags = 1

      }

New-ADObject -Name asFavoriteColor -Type attributeSchema -Path $schemapath -OtherAttributes $attributes

When we create that new object, we can see our new attribute!

get-adobject -SearchBase $schemaPath -Filter 'name -eq "asFavoriteColor"'

Image of command output

However, we aren’t quite done. We have an attribute, but it hasn’t been attached to the user class yet. To attach an attribute to an existing class, we have to update the class to include the attribute. This was not the easiest thing to figure out.

After looking at a few LDIF files, I finally realized that there was a property on the schemaClass objects called mayContain. This is a list of all the attributes associated with a particular class. From here, we can get the user schema object and add the new attribute to its mayContain property.

$userSchema = get-adobject -SearchBase $schemapath -Filter 'name -eq "user"'

$userSchema | Set-ADObject -Add @{mayContain = 'asFavoriteColor'} 

Now we can finally set and get the asFavoriteColor property on a user object.

Image of command output

We can wrap up all this in a nice function. I set this up so I can take data from the pipeline by property names, so I can easily import a bunch of attributes from a CSV file. I also added support for ShouldProcess and set the ConfirmImpact value to High. Here is my code:

Function New-AttributeID {

    $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")

    $AttributeID=[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])

    $oid

}

Function Update-Schema {

[CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')]

param(

[Parameter(Mandatory,ValueFromPipelinebyPropertyName)]

$Name,

[Parameter(Mandatory,ValueFromPipelinebyPropertyName)]

[Alias('DisplayName')]

$LDAPDisplayName,

[Parameter(Mandatory,ValueFromPipelinebyPropertyName)]

[Alias('Description')]

$AdminDescription,

[Parameter(Mandatory,ValueFromPipelinebyPropertyName)]

[Alias('SingleValued')]

$IsSingleValued,

[Parameter(ValueFromPipelinebyPropertyName)]

[Alias('OID')]

$AttributeID = (New-OID)

)

BEGIN {}

PROCESS {

  $schemaPath = (Get-ADRootDSE).schemaNamingContext      

  $type = 'attributeSchema'

  switch ($isSingleValued)

  {

   'True'  {$IsSingleValued = $true}

   'False' {$IsSingleValued = $false}

   default {$IsSingleValued = $true}

  }

  $attributes = @{

      lDAPDisplayName = $Name;

      attributeId = $AttributeID;

      oMSyntax = 20;

      attributeSyntax = "2.5.5.4";

      isSingleValued = $IsSingleValued;

      adminDescription = $AdminDescription;

      searchflags = 1

      }

    $ConfirmationMessage = "$schemaPath. This cannot be undone"

    $Caption = 'Updating Active Directory Schema' 

    if ($PSCmdlet.ShouldProcess($ConfirmationMessage,$Caption))

    {

      New-ADObject -Name $Name -Type $type -Path $schemapath -OtherAttributes $attributes

      $userSchema = get-adobject -SearchBase $schemapath -Filter 'name -eq "user"'

      $userSchema | Set-ADObject -Add @{mayContain = $Name}

    }

}

END {}

}

You can download this code from GitHub: PowerShell to Udpate Active Directory Schema.

This purpose of this series and code is to show you the basics of how to use native Windows PowerShell to extend the Active Directory schema. I would like to reiterate that any schema modifications should be well thought out and planned for. There are many more attributes that can be used and that could be useful. You can use this code as a basic framework for updating the schema with your company’s requirements and standards.

~Andy

Thank you Andy. This is some awesome material.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Author

The "Scripting Guys" is a historical title passed from scripter to scripter. The current revision has morphed into our good friend Doctor Scripto who has been with us since the very beginning.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • Jon Gillen

    This script is great for Windows Server 2012 R2 but it does not work in Server 2016

    • Jon Gillen

      I once again landed on this page via Google, looked over the script, tried to use it, found that it doesn’t work in Server 2016, then went to comment as much, and found out that I already made that comment a year ago. (-‸ლ)

    • AlanW

      this comment has been deleted.