24.1 Modifying the Schema with ADSI

We've shown you how the schema works in Chapter 4, and how to design extensions in Chapter 12. Now let's take a look at how to query and manipulate the schema using ADSI.

24.1.1 IADsClass and IADsProperty

In addition to being able to query and update schema objects as you can any other type of object with the IADs interface, there are two main schema-specific interfaces available: IADsClass and IADsProperty. Each of these interfaces has a variety of useful methods and property methods to allow you to set mandatory properties for classes, optional properties for classes, maximum values for attributes, and so on. If you look at these interfaces, you will see that they are very simple to understand.

First, let's compare accessing and modifying the schema by using the attributes we are interested directly in versus using the IADsClass and IADsProperty methods. This first code section uses attributes directly:

objAttribute.Put "isSingleValued", False
objAttribute.Put "attributeId", "1.3.6.1.4.1.999999.1.1.28"
   
arrMustContain = objSchemaClass.Get("mustContain")
arrMayContain = objSchemaClass.Get("mayContain")

Now we will use the ADSI schema interfaces to do the same thing:

objAttribute.MultiValued = True
objAttribute.OID = "1.3.6.1.4.1.999999.1.1.28"
   
arrMustContain = objSchemaClass.MandatoryProperties
arrMayContain = objSchemaClass.OptionalProperties

This makes use of IADsProperty::MultiValued, IADsProperty::OID, IADsClass::MandatoryProperties, and IADsClass::OptionalProperties. As you can see, it's not hard to convert the code. However, we feel that including code that directly modifies the properties themselves gives you some idea of what you are actually changing and helps you to refer back to the definitions presented in Chapter 4.

More details on these three interfaces can be found in the MSDN Library (http://msdn.microsoft.com/library/) under Networking and Directory Services Active Directory, ADSI and Directory Services SDK Documentation Directory Services Active Directory Service Interfaces ADSI Reference ADSI Interfaces Schema Interfaces.

24.1.2 Creating the Mycorp-LanguagesSpoken attribute

We will create an example attribute called Mycorp-LanguagesSpoken. It is to be a multivalued, indexed attribute that can hold an array of case-sensitive strings of between 1 and 50 characters. The name is prefixed with Mycorp so it is obvious that Mycorp created the attribute.

Mycorp's Schema Manager has decided that the OID for this attribute is to be 1.3.6.1.4.1.999999.1.1.28. This is worked out as follows:

  • Mycorp's root OID namespace is 1.3.6.1.4.1.999999.

  • Mycorp's new attributes use 1.3.6.1.4.1.999999.1.1.xxxx (where xxxx increments from 1).

  • Mycorp's new classes use 1.3.6.1.4.1.999999.1.2.xxxx (where xxxx increments from 1).

  • The attribute is to be the 28th new attribute created by Mycorp.

The code to create such an attribute is included in Example 24-1.

Example 24-1. Creating the MyCorp-LanguagesSpoken attribute
Dim objAttribute
Dim objSchemaContainer
   
Set objSchemaContainer = _
  GetObject("LDAP://cn=Schema,cn=Configuration,dc=mycorp,dc=com")
   
Set objAttribute = objSchemaContainer.Create("attributeSchema", _
                                      "cn=Mycorp-LanguagesSpoken")
   
'Write out mandatory attributes
objAttribute.Put "attributeId", "1.3.6.1.4.1.999999.1.1.28"
objAttribute.Put "oMSyntax", 20
objAttribute.Put "attributeSyntax", "2.5.5.3"
objAttribute.Put "isSingleValued", False
objAttribute.Put "lDAPDisplayName", "myCorp-LanguagesSpoken"
   
'Create the attribute
objAttribute.SetInfo
   
'Write out optional attributes
objAttribute.GetInfo
objAttribute.Description = "Indicates the languages that " & _
                           "a user speaks"
objAttribute.Put "rangeLower", 1
objAttribute.Put "rangeUpper", 50
objAttribute.Put "searchFlags", True
objAttribute.SetInfo

That was fairly straightforward. Remember to change the attributeID to correspond to your own OID namespace if you use the code. Figure 24-1 shows the newly created attribute using the Schema Manager snap-in.

Figure 24-1. The Mycorp-LanguagesSpoken attribute viewed using the Schema Manager snap-in
figs/ads2.2401.gif

24.1.3 Creating the FinanceUser class

We will now create a new class called Mycorp-FinanceUser. It is to be a structural class so that others can create instances of it within containers. It will have the new Mycorp-LanguagesSpoken as an attribute, as well as inheriting from the User class. The OID for the class will be 1.3.6.1.4.1.999999.1.2.4, representing the fourth class we've created under our base OID. Example 24-2 contains the code to create the class.

Example 24-2. Creating the FinanaceUser class
Const ADS_PROPERTY_APPEND = 3
   
Dim objAttribute
Dim objSchemaContainer
   
Set objSchemaContainer = _
  GetObject("LDAP://cn=Schema,cn=Configuration,dc=mycorp,dc=com")
   
Set objClass = objSchemaContainer.Create("classSchema", _
                                         "cn=Mycorp-FinanceUser")
   
'Write out mandatory attributes
objClass.Put "cn", "Mycorp-FinanceUser"
objClass.Put "governsId", "1.3.6.1.4.1.999999.1.2.4"
objClass.Put "objectClassCategory", 1 'Structural Class
objClass.Put "subClassOf", "user"
objClass.Put "lDAPDisplayName", "mycorp-FinanceUser"
   
'Create the class
objClass.SetInfo
   
'Write out optional attributes
objClass.GetInfo
objClass.Description = "Indicates a Financial User"
objClass.Put "mustContain", "1.3.6.1.4.1.999999.1.1.28"
objClass.SetInfo

Figure 24-2 is the Schema Manager view of the newly created Mycorp-FinanceUser class.

Figure 24-2. The Mycorp-FinanceUser class viewed using the Schema Manager snap-in
figs/ads2.2402.gif
24.1.3.1 Creating instances of the new class

Finally, we want to create a new Mycorp-FinanceUser object. First, we have to get a reference to the Schema Container and create the object with all the mandatory attributes. Example 24-3 shows what this would look like.

Example 24-3. Creating a reference to the Schema Container
Dim objContainer
Dim objMycorpFinanceUser
   
Set objContainer = _
  GetObject("LDAP://ou=Finance Users,dc=Mycorp,dc=com")
   
'Create the new Mycorp-FinanceUser object
Set objMycorpFinanceUser = objContainer.Create("Mycorp-FinanceUser", _
  "cn=SimonWilliams")
   
'Set the mandatory properties
objMycorpFinanceUser.Put "sAMAccountName", "SimonWilliams"
objMycorpFinanceUser.Put "userPrincipalName", "SimonWilliams@mycorp.com" 
objMycorpFinanceUser.Put "Mycorp-LanguagesSpoken", _
  Array("English", "French", "German")
   
'Write the object to the AD
objMycorpFinanceUser.SetInfo

Note that the mandatory properties include Mycorp-LanguagesSpoken from the Mycorp-FinanceUser class and sAMAccountName from the User class, which the Mycorp-FinanceUser class inherits from. UserPrincipalName is also included for completeness.

24.1.4 Finding the Schema Container and Schema FSMO

In your scripts or applications, it is good practice to locate the Schema Container and Schema FSMO dynamically instead of hardcoding those values. By finding those values programmatically, your scripts become much more forest-independent, which makes it much easier to transport to other forests in the future.

The solution to find the Schema Container is an easy one. The DN of the Schema Container for a forest can be found by querying the schemaNamingContext value of the RootDSE on any domain controller in the forest. The following code shows how to do that:

Dim objRootDSE
Dim objSchemaContainer
Dim strSchemaPath
   
'Get the Root DSE from a random DC
Set objRootDSE = GetObject("LDAP://RootDSE")
   
'Get the Schema NC path for the domain
strSchemaPath = objRootDSE.Get("schemaNamingContext")
   
'Connect to the schema container on a random DC
Set objSchemaContainer = GetObject("LDAP://" & strSchemaPath)

The first GetObject call retrieves the RootDSE. Next we simply get the schemaNamingContext attribute and pass that to another GetObject call (or the IADsOpenDSObject::OpenDSObject method if you prefer to authenticate), which will return a reference to the Schema Container on a random domain controller. If you want to make changes without forcing the FSMO role to your currently connected server, you need to change the last line to connect to the server currently holding the Schema FSMO. This can be done in three additional steps:

Set objSchemaContainer = GetObject("LDAP://" & strSchemaPath)
strFSMORoleOwner =  objSchemaContainer.Get("fSMORoleOwner")
   
Dim objNTDS, objServer
Set objNTDS = GetObject("LDAP://" &  objSchemaContainer.Get("fSMORoleOwner") )
Set objServer = GetObject( objNTDS.Parent )
strFSMORoleOwner = objServer.Get("dNSHostName")
   
'Connect to the schema container on the server holding the FSMO Schema
'Master role
Set objSchemaContainer = _
  GetObject("LDAP://" & strServerIPName & "/" & strSchemaPath)

The fSMORoleOwner attribute of the Schema Container actually contains the NTDS Settings DN (e.g., cn=NTDS Settings,cn=MOOSE,cn=Servers,cn=Main-Headquarters-Site,cn=Sites,cn=Configuration,dc=mycorp,dc=com) of the domain controller holding the Schema FSMO. From this you can retrieve the ADsPath of the parent container which holds an attribute called dNSHostName that contains the DNS host name of the domain controller that object represents.

24.1.5 Transferring the Schema FSMO Role

If you want to transfer the Schema FSMO role to a specific server, just set the becomeSchemaMaster attribute to 1 on the RootDSE for that server. The script will need to either run under the credentials of someone in the Schema Admins group to perform this transfer or use IADsOpenDSObject::OpenDSObject and authenticate as someone in Schema Admins. The moment we write out the property cache, the proposed master contacts the current master and requests the role and any updates to the Schema NC that it has yet to see. Here is the code to do the transfer:

Const DC_TO_TRANSFER_FSMO_TO = "niles.mycorp.com"
   
Dim objRootDSE
Dim objSchemaContainer
Dim strSchemaPath
   
'Get the Root DSE from a random DC
Set objRootDSE = GetObject("LDAP://" & DC_TO_TRANSFER_FSMO_TO & _
                           "/RootDSE")
   
'Request a Schema Master transfer
objRootDSE.Put "becomeSchemaMaster", 1
objRootDSE.SetInfo

At this point, the transfer has been requested. We now need to connect to the Schema NC and wait until the fSMORoleOwner attribute points to our new server:

'Get the Schema NC path for the domain
strSchemaPath = objRootDSE.Get("schemaNamingContext")
   
'Connect to the schema container on my DC
Set objSchemaContainer = GetObject("LDAP://" & DC_TO_TRANSFER_TO _
  & "/" & strSchemaPath)
   
'Initialize the while loop by indicating that the server is not the one
'I am looking for
strServerName = ""
   
'While the Server Name is not the one we are looking for, keep searching
While Not strServerName = DC_TO_TRANSFER_FSMO_TO
  'Get the FSMO Role Owner attribute
  strFSMORoleOwner =  objSchemaContainer.Get("fSMORoleOwner")
   
  Set objNTDS = GetObject("LDAP://" &  strFSMORoleOwner )
  Set objServer = GetObject( objNTDS.Parent )
  strServerName = objServer.Get("dNSHostName")
   
  objNTDS = Nothing
  objServer = Nothing
Wend
   
'At this point in the code, the role has been transferred, so I can continue

You shouldn't use the code exactly as written here because no error checking is being done. Without error checking, there is no guarantee that the original writing of the becomeSchemaMaster attribute actually worked. There is also no guarantee that the attachment to the DC_TO_TRANSFER_FSMO_TO server actually worked, either. So if either of these or anything else went wrong, we may never exit the while loop. Even if both of these conditions worked, we may set the value, and the DC may attempt to contact the current Schema FSMO to find that it is unavailable. Again, we go into an infinite loop and the code never terminates. You certainly should include a timeout value as a second condition to the while loop to trap an occurrence of this problem.

24.1.6 Forcing a Reload of the Schema Cache

If you need to reload the schema cache, Microsoft recommends that you do so once you've finished all your writes. While the cache is being reloaded, any new queries are served from the old cache and will continue to be served by the old cache until the new one comes online. Microsoft specifically states that working threads that are referencing the old cache once a reload is finished will continue to reference the old cache. Only new threads will reference the new cache. As a worst-case (and really daft) scenario, if you were to create 100 new attributes, start a process that queried the schema cache, and then reload the schema cache before continuing on to the next attribute, you potentially have 101 sets of schema caches (the original plus 100 new ones) being maintained by the DC acting as the Schema FSMO. This would cause the DC to have 101 times the amount of normal schema cache memory in use for caching. This is likely to cause a drain on your DC. In this scenario, things will only improve as the working threads cease referencing the old caches on the DC, allowing it to free up the memory.

Reloading the cache using ADSI is very simple. All you have to do is set the schemaUpdateNow attribute to 1 on the RootDSE of a DC. The following code shows how to do this:

Dim objRootDSE
Dim strDC
   
strDC = "dc01"
   
'Get the Root DSE
Set objRootDSE = GetObject("LDAP://" & strDC & "RootDSE")
   
'Reload the cache on that DC
objRootDSE.Put "schemaUpdateNow", 1

Note that just because you have requested a change doesn't mean it's going to happen instantaneously, although your code will continue executing. You should check the schema to see if your new objects are there, and if they are not, wait until they are before proceeding.

24.1.7 Finding Which Attributes Are in the GC for an Object

In Chapter 4, we described a desire to programmatically query all attributes directly defined on an object class in the schema to find out which ones are in the GC. It now should be possible to see how simple it is to write this code. First, you know that you can find out which attributes a classSchema object can have by looking in the mayContain, systemMayContain, mustContain, and systemMustContain attributes. Once you have the entire list of attributes, you can use the lDAPDisplayName that you will have retrieved to reference the attributeSchema class in the Schema Container. Lastly, you need to check to see whether each attributeSchema object has an attribute called isMemberOfPartialAttributeSet. Example 24-4 contains the code.

Example 24-4. Checking for the isMemberOfPartialAttributeSet attribute
'Check the User class, via the administrator user
Const OBJECT_TO_CHECK = _
  "LDAP://cn=administrator, cn=Users, dc=Mycorp, dc=com"
   
Dim objObject, objSchemaClass, arrMustContain, arrMayContain
Dim strListOfAttributesinGC, objAttribute, strAttribute
   
'Connect to object and get IADs::Schema on an object instance
Set objObject = GetObject(OBJECT_TO_CHECK)
   
Set objSchemaClass = GetObject(objUser.Schema)
   
'Get May-Contain and Must-Contain attributes directly on the class
arrMustContain = objSchemaClass.Get("mustContain")
arrMayContain = objSchemaClass.Get("mayContain")
   
'Initialize the output string
strListOfAttributesinGC = "The list of attributes for the class: " & vbCrLf _
  & vbCrLf & vbTab & objUser.Schema & vbCrLf & vbCrLf & "is:"_
  & vbCrLf & vbCrLf
   
'Use the array of LDAP names to connect to each attribute in turn
'and read whether it is in the GC or not. If it is, then add it to the string
For Each strAttribute In arrMustContain
  Set objAttribute = GetObject("LDAP://" & strAttribute & _
    ",cn=Schema,cn=Configuration,dc=Mycorp,dc=com")
  If objAttribute.Get("isMemberOfPartialAttributeSet") Then
    strListOfAttributesinGC = strListOfAttributesinGC & _
                              strAttribute & vbCrLf
  End If
   
  'Stop referencing the current attribute
  Set objAttribute = Nothing
Next
   
For Each strAttribute In arrMayContain
  Set objAttribute = GetObject("LDAP://" & strAttribute & _
    ",cn=Schema,cn=Configuration,dc=Mycorp,dc=com")
  If objAttribute.Get("isMemberOfPartialAttributeSet") Then
    strListOfAttributesinGC = strListOfAttributesinGC & _
                              strAttribute & vbCrLf
  End If
   
  'Stop referencing the current attribute
  Set objAttribute = Nothing
Next
   
Wscript.Echo strListOfAttributesinGC

This will print out a list of all attributes that are held in the GC and defined directly on the object. Of course, to be very thorough and find every attribute that the classSchema object could have in the GC, you also would need to look inside auxiliaryClass, systemAuxiliaryClass, and subClassOf to retrieve any class names and then iterate back up the tree to find the mayContain, systemMayContain, mustContain, and systemMustContain from any inherited classes.

24.1.8 Adding an Attribute to the GC

If you identify an attribute that you would like to be part of the GC, but it is not, it is straightforward to add it. Typically, an attribute that is part of the GC should be "globally interesting," meaning more than one application would find use for it. The data for the attribute also should not be very volatile or very large since it will be replicated to every GC server in the forest. To add an attribute, the isMemberOfPartialAttributeSet attribute for the attributeSchema object must be set to true. Example 24-5 shows how to enable the myCorp-SpokenLanguages attribute in the GC.

Example 24-5. Enabling the myCorp-SpokenLanguages attribute
Const ATTR_TO_GC = "myCorp-LanguagesSpoken"
   
Dim objRootDSE, objSchemaContainer
Dim strFSMORoleOwner, strSchemaPath
   
'Get the Root DSE from a random DC
Set objRootDSE = GetObject("LDAP://RootDSE")
   
'Get the Schema NC path for the domain
strSchemaPath = objRootDSE.Get("schemaNamingContext")
   
'Connect to the schema container on a random DC
Set objSchemaContainer = GetObject("LDAP://" & strSchemaPath)
strFSMORoleOwner =  objSchemaContainer.Get("fSMORoleOwner")
   
'Get the Schema FSMO DNS name
Dim objNTDS, objServer
Set objNTDS = GetObject("LDAP://" &  objSchemaContainer.Get("fSMORoleOwner") )
Set objServer = GetObject( objNTDS.Parent )
strFSMORoleOwner = objServer.Get("dNSHostName")
   
'Get the attribute 
Set objSchemaContainer = _
  GetObject("LDAP://" & strServerIPName & "/" & strSchemaPath)
   
Dim objAttr
Set objAttr = GetObject("LDAP://" & strFSMORoleOwner & "," & _
                  "cn=" & ATTR_TO_GC & "," & strSchemaPath
   
'Set the property to true 
objAttr.Put "isMemberOfPartialAttributeSet", True
objAttr.SetInfo

Under Windows 2000, anytime an attribute is added to the GC, a full sync of the GC contents is initiated to all GC servers in the forest. Since this can have a significant impact on replication and network performance, it should be done with caution. This limitation was removed in Windows Server 2003.



    Part II: Designing an Active Directory Infrastructure
    Part III: Scripting Active Directory with ADSI, ADO, and WMI