Extending the Schema

The Active Directory schema can be modified in several ways: new attributes and classes can be created, and existing ones can be disabled (but not deleted). You cannot disable core classes and attributes (called system classes or system attributes) that Active Directory needs. Adding classes and attributes is more common than disabling them.

Since Active Directory includes so many root classes and attributes, you don't often need to extend it. However, "never say never" is a good motto in the computer industry, and the designers of Active Directory, in classic Microsoft fashion, built a system in which modifying the schema is relatively easy.

But don't let the ease of modifying the schema entice you along a path of unnecessary work. Just because you can, doesn't mean you should. The purpose of this section is not only to show you how to modify the schema but to point out the impact and pitfalls of doing so.

The Process for Extending the Schema

The steps for extending the schema are as follows:

  1. Decide whether to extend the schema.
  2. Determine the method of extension.
  3. Enable schema changes.
  4. Obtain an OID.
  5. Create schema objects.
  6. Update the schema cache.

As an example, I'm going to design a schema extension that creates a new attribute and a new class that uses the new attribute. The example, which is very simple, provides a way for me to store the titles and authors of the many books I own.

When to Extend the Schema

You typically extend the schema when the existing classes and attributes do not fit with the type of data you want to store. Over the past year, I've worked with several developers who wanted to know how to extend the schema to fit some particular need they had. However, in many cases, Active Directory already contained a class or attribute in its default schema that fit the bill. Reviewing the existing schema carefully to see whether a class or attribute already exists is really important.

Of particular importance is the fact that schema additions are permanent. Let me say that again: Although you can add new attributes and classes, you can never remove them from the schema. You can disable classes and attributes, but they are not removed from the directory. Keep this in mind when testing your code.

What Data Is Best Stored in Active Directory?

Generally, directory services work best with data that is read more often than it is written. Since directory data must be replicated to other servers, an inherent latency is involved. If the data being stored is updated frequently and must be current on all the directory servers, Active Directory might not be the best place to store it.

The next release of Windows 2000, code-named Whistler, includes a new feature called dynamic objects. By using a new auxiliary class, a class can be created that contains a time-to-live (TTL) value. The TTL specifies, in seconds, how long the particular instance of an object remains in the directory. When the TTL reaches zero, the instance is deleted. See Chapter 11 for more information about this feature.

Another consideration when deciding whether to extend the schema is the size of the data you want to store. The smaller the better is the general rule. Microsoft recommends that no attribute value exceed 500 KB, including the sum of multivalued attributes. Additionally, no object should exceed 1 MB. I think these recommendations are excessively lenient. If you are considering adding an attribute that might contain 500 KB of data, maybe you should consider storing a pointer to the data in Active Directory and keep the data itself in a different store. Of course, you have to balance the maximum size of an attribute or class with the expectation of how many instances will be created in the directory. A 100-KB attribute might not be a big deal for a few objects. However, if you're attaching that attribute to, say, the user class, and there are 100,000 or more instances in the directory, watch out. The size of the data store might be prohibitive, and the replication traffic might swamp your network's available bandwidth.

Here is a real-world example. While working on Microsoft Exchange 2000 Server, I looked into the feasibility of storing short audio clips as an attribute of a user object. The idea was to provide an auditory version of a person's name when sending a message over a telephone voice-mail system. Even the most compressed audio clips took up at least 10 KB of space for each user object. Another problem was that the information was not streamed when it was needed; it had to be transferred in a block operation. This limitation resulted in small but noticeable delays at the client side. In the end, we decided to store the actual clip in the Exchange Web Storage System and provide an attribute with a URL pointer to the clip. The Web Storage System could use streaming techniques to efficiently provide multimedia content of any size without negatively affecting the performance of Active Directory.

Any time you want to create large attributes to store binary data, consider whether it would be better to use the attribute value as a pointer to the data and locate the data on a different server. Of course, the downside is making sure that the data is as widely available as Active Directory data.

Determining the Method of Extension

I cannot stress enough that you must carefully design your schema changes. Once you have done that, the next step is deciding which method to use to extend the schema. You can make schema changes using the following methods:

  • Manually using the Active Directory Schema snap-in
  • Manually using import files
  • Programmatically using an installation program

Which method you use depends on the scope of the changes you want to make. For the addition of one or a few attributes or classes, the Active Directory Schema snap-in is easiest, as it presents a graphical user interface. If the changes are moderate, or you need to make them to a number of installations of Active Directory, using import files is a possibility. These files are in text format and can be easily edited. I discuss them in more detail later in the section "Using Import Files." A disadvantage of using import files is that they can be tampered with, and the ability to recover from errors is limited. You can solve those problems by extending the schema programmatically as part of your application's setup program. This is the method I recommend for directory-enabled applications.

Enabling Schema Changes

Regardless of the method you are using to modify the schema, you will need to enable schema changes. To ensure that changes to the schema are not made haphazardly, Windows 2000 imposes three safety interlocks that control modifications to the Active Directory schema.

  • To prevent potential replication conflicts, only one domain controller in the organization is allowed to write to the schema. This server is known as the schema operations master or schema master. Changes can be made only to this domain controller's copy of the schema.
  • The security permissions of the Schema container object are set so that only members of the Schema Admins group can modify the contents of the schema container.
  • A registry setting on all Windows 2000 domain controllers is used to enable schema changes. By default, this registry setting is set to prevent schema changes, even at the schema master.

The following sections describe each safety interlock and how to work with them in a program.

Connecting to the Schema Master

In many of the samples in this book, I use a practice known as serverless binding. Since Active Directory is distributed to all domain controllers, it doesn't matter which one you connect to. However, when updating the schema, you must connect to the domain controller that is acting in the role of schema master because only that domain controller is allowed to modify the schema. The first thing you need to know is which domain controller is performing that role, and then you need its full Domain Name System (DNS) address. With that information, you can bind directly to the schema on that domain controller and perform the modifications.

Some texts and articles on Active Directory recommend programmatically changing the schema master to be the one that is executing your code. That avoids having to figure out which domain controller is the schema master and remotely connecting to it. However, I think doing this is bad form. There might be a good reason why a particular domain controller is serving as the schema master and an application should not alter that, even if it restores the original settings afterward.

The process of figuring out the DNS name of the domain controller serving as the schema master involves several steps and is a little convoluted. ADSI provides some relief by providing the ADSystemInfo object and IADsADSystemInfo interface, which you can use to discover a number of pieces of information about the enterprise Active Directory.

The SchemaRoleOwner property of ADSystemInfo returns the distinguished name of the NTDS Settings object of the server that is acting as the schema master. By retrieving the parent of this object and binding to it, you can get the full address of the schema master domain controller. Listing 9-6, from the ExtendSchema sample on the companion CD, shows the ReturnSchemaMaster function, which retrieves the name of the schema master.

 //----------------------------------------------------------------
// Function:     ReturnSchemaMaster
// Description:  This function looks up the DC holding the schema 
//               master FSMO role for the Active Directory forest 
//               and returns its fully qualified DNS name.
//
// In/Out:       _bstr_t* String to hold schema master address
// Returns:      HRESULT  COM/ADSI error codes.
//
// Note:         ADSystemInfo only supported on Windows 2000
//----------------------------------------------------------------
HRESULT ReturnSchemaMaster( _bstr_t *pbstrSchemaMasterFQDN )
{
HRESULT hResult;
// Create an ADSystemInfo object
IADsADSystemInfo *padsADSysInfo;
hResult = CoCreateInstance( CLSID_ADSystemInfo,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IADsADSystemInfo,
    (void**) &padsADSysInfo );
if ( SUCCEEDED( hResult ) )
    {
    // Retrieve the SchemaRoleOwner attribute
    // The returned DN points to the NTDS Settings object for
    // the DC acting as the schema master.
    BSTR bstrSchemaDN;
    hResult = padsADSysInfo->get_SchemaRoleOwner ( &bstrSchemaDN );
    if( SUCCEEDED( hResult ) )
        {
        // Create ADsPath to the schema master NTDS object
        _bstr_t bstrSchemaMasterNTDS = _bstr_t( "LDAP://" ) +
            bstrSchemaDN;
        // Bind to the schema NTDS object
        IADs *padsSchemaNTDS = NULL;
        hResult = ADsGetObject( bstrSchemaMasterNTDS,
            IID_IADs,
            (void**) &padsSchemaNTDS );         if ( SUCCEEDED ( hResult ) )
            {
            // Get the parent of the NTDS object
            BSTR bstrParent;
            hResult = padsSchemaNTDS->get_Parent ( &bstrParent );
            // The parent is the schema master 
            IADs *padsSchemaMaster = NULL;
            hResult = ADsGetObject( bstrParent,
                IID_IADs,
                (void**) &padsSchemaMaster );
            if ( SUCCEEDED ( hResult ) )
                {
                // Get the actual DNS address
                _variant_t varDNS;
                hResult = padsSchemaMaster->Get ( L"dNSHostName",
                    &varDNS );
                // Return the FQDN address
                *pbstrSchemaMasterFQDN = _bstr_t( varDNS );
                }
            // Free the schema master object
            if ( padsSchemaMaster )
                padsSchemaMaster->Release ();
            // Free the parent string
            SysFreeString( bstrParent );
            }
        // Free the RootDSE object
        if ( padsSchemaNTDS )
            padsSchemaNTDS->Release ();
        }
    // Free the object-allocated string
    SysFreeString( bstrSchemaDN );
    }
// Release object
if ( padsADSysInfo )
    padsADSysInfo->Release ();
return hResult;
}

Listing 9-6 The ReturnSchemaMaster function from the ExtendSchema sample showing how to retrieve the name of the schema master.

The ReturnSchemaMaster function is used by the BindToSchemaMaster function in Listing 9-7 to bind to the schema partition on the schema master domain controller:

 //----------------------------------------------------------------
// Function:     BindToSchemaMaster
// Description:  Bind to the schema container on the schema 
//               master server
//
// In/Out:       IADs**    Pointer to schema container
//               _bstr_t*  String to hold server name
// Returns:      HRESULT   COM/ADSI error codes
//----------------------------------------------------------------
HRESULT BindToSchemaMaster(IADs **padsSchema, 
    _bstr_t *pbstrSchemaMasterFQDN )
{
HRESULT hResult;
// Bind to the RootDSE
IADs *padsRootDSE = NULL;
hResult = ADsGetObject( L"LDAP://rootDSE",
     IID_IADs,
     (void**) &padsRootDSE );
if( SUCCEEDED( hResult ) )
    {
    // Get the schema naming context DN
    _variant_t varSchemaNC;
    hResult = padsRootDSE->Get( L"schemaNamingContext", &varSchemaNC );
    if( SUCCEEDED( hResult ) )
        {
        // Retrieve the FQDN of the schema master
        ReturnSchemaMaster( pbstrSchemaMasterFQDN );
        // Create ADsPath to the schema NC on the schema master server
        _bstr_t strSchemaMasterPath = bstr_t( "LDAP://" ) +
            *pbstrSchemaMasterFQDN +
            _bstr_t( "/" ) +
            _bstr_t(varSchemaNC);         // Bind to the schema container, return IADs interface
        hResult = ADsGetObject( strSchemaMasterPath,
            IID_IADs,
            (void**) padsSchema );
        }
    }
// Free the RootDSE object
if ( padsRootDSE )
    padsRootDSE->Release ();
// Return the result code
return hResult;
}

Listing 9-7 The BindToSchemaMaster function from the ExtendSchema sample showing how to bind to the Schema container on the schema master.

Verifying Correct Permissions

When preparing to extend the schema, your installation program must verify that it can actually create objects in the Schema container. The user executing the installation program, by default, must be a member of the Schema Admins group. (You could also run the installation program using the Run As command and provide the name and password of a user who is a member of Schema Admins.)

To prevent errors while creating schema objects, it's best to check early on whether sufficient privileges are available. Since system administrators might change the security settings for the Schema Admins group, verify the permissions by examining the allowedChildClassesEffective attribute of the Schema container. This multivalued attribute contains the names of each class of object that can be added by the current user. The two types of objects we're interested in creating are attributeSchema and classSchema objects to represent the additions we're making to the schema. If either or both of these objects are not part of allowedChildClassesEffective, the current execution context does not have sufficient privileges and the schema extension program should warn the user and exit. Alternatively, the installation program can prompt for a different set of credentials to be used and then verify against them.

Listing 9-8 shows the VerifySchemaPermissions function from the Extend-Schema sample on the companion CD. It demonstrates the method used to verify permissions, and it returns a result indicating whether the installation program has sufficient rights to modify the schema.

 //----------------------------------------------------------------
// Function:     VerifySchemaPermissions
// Description:  Verifies that current execution context has 
//               proper permissions to update schema.  
//
// In/Out:       IADs* IADs Pointer to schema container
// Returns:      HRESULT S_OK if allowed to modify schema
//               S_FALSE if not allowed to modify schema
//               E_POINTER if passed an invalid pointer
//               Other COM/ADSI result codes possible
//----------------------------------------------------------------
HRESULT VerifySchemaPermissions( IADs *padsSchema )
{
HRESULT hResult;
// Verify that pointer is valid
if ( !padsSchema )
    return E_POINTER;
//------------------------------------------------
// Use GetInfoEx to retrieve constructed attribute
//------------------------------------------------
// Create variant array to hold attributes to retrieve
wchar_t* prgstrAttributes[] = { L"allowedChildClassesEffective" };
_variant_t varAttributes;
hResult = ADsBuildVarArrayStr( prgstrAttributes, 
    ARRAYSIZE(prgstrAttributes), 
    &varAttributes);
if (SUCCEEDED( hResult ) )
    {
    // Retrieve attribute and place in property cache
    hResult = padsSchema->GetInfoEx ( varAttributes, 0L );
    if ( SUCCEEDED( hResult ) )
        {
        // Get all the allowed classes
        _variant_t varAllowedClasses;
        hResult = padsSchema->GetEx( L"allowedChildClassesEffective",
            &varAllowedClasses);
        // Did we get values back?  Are they in an array?
        if ( SUCCEEDED ( hResult ) && 
            V_ISARRAY( &varAllowedClasses ) )
            {
            // Create a safe array
            SAFEARRAY *saClasses = V_ARRAY( &varAllowedClasses );             // Setup the upper and lower boundries of the array
            long lMin;
            long lMax;
            SafeArrayGetLBound( saClasses, 1, &lMin );
            SafeArrayGetUBound( saClasses, 1, &lMax );
            // Variables are set when class found
            bool bAttributeAllowed = false;
            bool bClassAllowed = false;
            // Enumrate each allowed class 
            for ( long nIndex = lMin; 
                nIndex <= lMax;
                nIndex++ )
                {
                // Create a variant to hold the current value
                _variant_t varClass;
                // Use the Automation helper function to get the value
                hResult = SafeArrayGetElement( saClasses, 
                    &nIndex,
                    &varClass );
                if ( SUCCEEDED( hResult ) )
                    {
                    // Is current value the attributeSchema class?
                    if ( _wcsicmp( L"attributeSchema",
                        _bstr_t(varClass) ) == 0)
                        {
                        bAttributeAllowed = true;
                        }
                    else
                        {
                        // Is current value the classSchema class?
                        if ( _wcsicmp( L"classSchema",
                            _bstr_t(varClass) ) == 0)
                            {
                            bClassAllowed = true;
                            }
                        }
                    }
                }
            // Return S_OK if both are allowed, otherwise S_FALSE
            if ( bAttributeAllowed && bClassAllowed )
                hResult = S_OK;
            else
                hResult = S_FALSE;
            }
        }
    }
// Return linger result
return hResult;
}

Listing 9-8 The VerifySchemaPermissions function from the ExtendSchema sample showing how to verify whether the current user can update the schema.

Modifying the Registry

The final step to enabling schema changes is to modify the Schema Update Allowed registry key on the schema master. You can use the Active Directory Schema snap-in or the Registry Editor, or you can change this setting programmatically. Using the Active Directory Schema snap-in is the easiest way to enable schema changes:

  1. In the Active Directory Schema snap-in, select the root entry, named Active Directory Schema.
  2. Click the Action menu, and then choose Operations Master. The Change Schema Master dialog box appears.

  1. Select the check box titled The Schema May Be Modified On This Domain Controller.
  2. Click OK.

If you are creating an application that extends the schema, you want to do this programmatically from your setup program. The specific value that forms the last safety interlock is in the registry of the schema master, at the following location:

 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters 

The specific value is a DWORD and is named Schema Update Allowed. If Schema Update Allowed is not present or has a value of 0, updates are not allowed. A value of 1 enables schema changes for that domain controller. Only the domain controller that is acting as the schema master needs this value set. The other domain controllers will accept schema updates as part of the standard replication process.

Listing 9-9 shows the EnableSchemaUpdates function from the Extend-Schema sample on the companion CD. This function connects to the registry on the specified computer and returns the current value of Schema Update Allowed. Optionally, the function can set Schema Update Allowed, creating the value if necessary.

 //----------------------------------------------------------------
// Function:     EnableSchemaUpdates
// Description:  Connect to registry on remote computer and update 
//               registry to enable Active Directory schema changes.
//
// In:           _bstr_t  Computer name (NetBIOS or FQDN address)
// In/Out:       DWORD*   0 to disable updates
//               1 to enable updates
//               -1 to read current value
//               On exit, contains the previous value of the key
// Returns:      HRESULT Win32 error code
//----------------------------------------------------------------
HRESULT EnableSchemaUpdates( bstr_t bstrComputerName, 
    DWORD *pdwEnableUpdates )
{
// Presume error until reset
long int lResult = E_FAIL;
// Save requested action
DWORD dwAction = *pdwEnableUpdates;
// Ensure string is valid
if ( bstrComputerName.length() == 0 )
    return E_INVALIDARG;
// Connect to registry on computer
HKEY hkHandle;
lResult = RegConnectRegistry( bstrComputerName,
    HKEY_LOCAL_MACHINE,
    &hkHandle);
if (lResult == ERROR_SUCCESS)
    {
    // Open the registry key for reading
    HKEY hkTarget;
    lResult = RegOpenKeyEx( hkHandle, 
        _T("System\\CurrentControlSet\\Services\\NTDS\\Parameters"), 
        0,
        KEY_READ,
        &hkTarget);     if (lResult == ERROR_SUCCESS)
        {
        // Query for the value
        DWORD dwType;
        DWORD dwSize = sizeof( DWORD );
        lResult = RegQueryValueEx( hkTarget, 
            _T("Schema Update Allowed"),
            0,
            &dwType,
            (LPBYTE)pdwEnableUpdates,
            &dwSize);
        // Is update requested?
        if ( dwAction != -1 )
            {
            // Close handle to key because it's for read access only
            RegCloseKey( hkTarget );
            // Attempt to open key for writing
            lResult = RegOpenKeyEx( hkHandle,
                _T("System\\CurrentControlSet\\Services\\NTDS\\Parameters"), 
                0, 
                KEY_WRITE,
                &hkTarget);
            if ( lResult == ERROR_SUCCESS )
                {
                // Set the value as desired
                lResult = RegSetValueEx( hkTarget,
                    _T("Schema Update Allowed"),
                    0L,
                    REG_DWORD,
                    (LPBYTE)&dwAction,
                    dwSize);
                }
            }
        // Close the handle to the key
        RegCloseKey (hkTarget);
        }
    // Close the handle to the remote registry
    RegCloseKey (hkHandle);
    }
return lResult;
}

Listing 9-9 The EnableSchemaUpdates function from the ExtendSchema sample showing how to read or modify the Schema Update Allowed value in the registry.

Do not leave the registry unlocked! When you are calling the EnableSchemaUpdates function to modify the registry, be sure to save the previous value, which is returned in the pdwEnableUpdates parameter. When finished with the schema modification, call EnableSchemaUpdates again with the previous value to return the registry to its previous state. The ExtendSchema sample shows how to do this.

Obtaining an Object Identifier

As I discussed at the beginning of this chapter, object identifiers (OIDs) are the unique identifiers for every attribute and class defined in the schema. For each new class and attribute you want to create, an OID must be obtained. There are two basic methods for obtaining an OID: getting a block of numbers from the Internet Assigned Number Authority (IANA) or using the numbers from the Microsoft-assigned block.

If you are going to be creating a lot of new classes or attributes, it's probably better to register with IANA and get your own branch of the global OID tree. IANA will assign numbers from the Private Enterprise Numbers branch of the OID tree. You can also register with ANSI in the United States to get a block of numbers for the US Organizations branch of the OID tree. ANSI charges a fee for this service, however. See the following Web sites for more information:

IANA

http://www.iana.org/

ANSI Registration

http://www.ansi.org/public/services/reg_org.html

For outside the United States, check the list of ISO members

http://www.iso.ch/addresse/membodies.html

The numbers you receive are yours to do with as you please. You can issue subtrees to others, but ultimately you are responsible for the management of this block of numbers. If you work with a lot of OIDs, take steps to manage your own numbers so that you don't accidentally reuse one. You could create a database of the numbers you assign and store them in a spreadsheet—or better yet, in Active Directory!

The easier and better way for making occasional schema extensions is to use a branch of numbers that Microsoft reserves for this purpose. Microsoft's branch of the OID tree for Active Directory purposes is 1.2.840.113556.1, and Microsoft has reserved two additional trees for Active Directory schema extensions. They are:

  • 1.2.840.113556.1.4, for attributes
  • 1.2.840.113556.1.5, for classes

The Microsoft Windows 2000 Resource Kit includes the Oidgen.exe utility for creating a unique number within the Microsoft branch. Of course, when Microsoft and unique number appear in the same sentence, a GUID isn't too far away. To keep themselves out of the business of managing all the numbers in their own branch of the OID tree, Microsoft uses GUIDs to create a unique number to tack onto the end of the Microsoft-allocated branch. The Oidgen utility actually creates two numbers, one for classes in the 1.2.840.113556.1.5 branch and another for attributes in the 1.2.840.113556.1.4 branch. Figure 9-9 shows the output of the Oidgen tool.

Figure 9-9 Using Oidgen to create unique base-object identifiers for new attributes and classes.

While in theory you could run Oidgen to generate a new OID every time you want to extend the schema, doing so is inefficient. Internally, Active Directory keeps a table of unique OID prefixes (the portion of the OID without the last branch). By doing so, it can quickly use the last branch number as an index value to speed up referencing of the entire OID. Creating a new branch for each new class or attribute will increase the size of the table and reduce Active Directory's performance.

A better solution is to simply use the Oidgen tool to set the number for your own branch and then assign numbers within that branch. For this sample, we need OIDs for one class and one attribute. Using the numbers assigned by Oidgen as a base, I'll use the OIDs listed in Table 9-13 for our new schema objects.

Object Identifier Common Name

1.2.840.113556.1.5.7000.111.28688.286 84.8.204138.830347.950265.1272930.1

coppersoft-Book

1.2.840.113556.1.4.7000.233.28688.286 84.8.51404.1012371.312491.931313.1

coppersoft-Authors

Table 9-13 A class OID and an attribute OID generated with Oidgen.

Schema objects must have a name prefix that is unique, which is usually your company name or domain name. Microsoft keeps a registry of schema naming prefixes for companies that want to extend their schemas. If you want your application to be considered for the Certified for Windows logo program, you must register your naming prefix with Microsoft. For more information, refer to the Application Specification for Microsoft Windows 2000 included on the companion CD and the registration Web site at http://msdn.microsoft.com/certification/ad-registration.asp. The companion CD also includes the SchemaDoc tool to help you document your own schema extensions in XML.

Creating Schema Objects

The next step is to actually create the new schema objects. As mentioned earlier, this can be done in various ways. You can use the Active Directory Schema snap-in, use import files, or create the objects programmatically.

Using the Active Directory Schema Snap-in

Using the Active Directory Schema snap-in is the easiest way to create new schema objects. In the Active Directory Schema snap-in, select the Attributes folder or the Classes folder, click the Action menu, point to New, and select either Attribute or Class. After clicking Continue in the warning dialog box, the Create New Attribute or Create New Schema Class dialog box is displayed. Using these dialog boxes, you can add the necessary information and a new attribute or class will be created. Figure 9-10 shows the Create New Attribute dialog box.

Figure 9-10 Creating a new attribute object using the Active Directory Schema snap-in.

Using Import Files

Windows 2000 includes a command-line tool named Ldifde.exe that can be used to obtain detailed information about existing classes or attributes or to modify or create classes or attributes. You can find this tool in the %WinDir%\System32 folder of Windows 2000 Server or Windows 2000 Advanced Server. Ldifde.exe imports or exports text files that are in the LDAP Data Interchange Format (LDIF). Listing 9-10 shows a sample LDIF file that creates a new attribute named coppersoft-Authors-v2 and a new class named coppersoft-Book-v2. The new attribute must be created first because the new class depends on its existence to be created.

 # Create multi-valued attribute to hold author names
# (wrap lines using a single space after CR/LF)
dn: CN=coppersoft-Authors-v2,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=com
changetype: add
adminDisplayName: coppersoft-Authors-v2
attributeID: 
 1.2.840.113556.1.4.7000.233.28688.28684.8.51404.1012371.312491.931313.2
attributeSyntax: 2.5.5.12
cn: coppersoft-Authors-v2
instanceType: 4
isSingleValued: FALSE
lDAPDisplayName: coppersoft-Authors-v2
distinguishedName: CN=coppersoft-Authors-v2,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
objectClass: attributeSchema
oMSyntax: 64
showInAdvancedViewOnly: TRUE
# Instruct Active Directory to reload the schema cache
# Must do this or creation of the class, below, will fail
# Use the schemaUpdateNow operational attribute of the RootDSE
DN:
changeType: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-  # Add the Book class 
dn: CN=coppersoft-Book-v2,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
changetype: add
adminDisplayName: coppersoft-Book-v2
cn: coppersoft-Book-v2
defaultObjectCategory: CN=coppersoft-Book-v2,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
governsID: 
 1.2.840.113556.1.5.7000.111.28688.28684.8.204138.830347.950265.1272930.2
instanceType: 4
lDAPDisplayName: coppersoft-Book-v2
mayContain: coppersoft-Authors-v2
distinguishedName: CN=coppersoft-Book-v2,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
objectCategory: CN=Class-Schema,CN=Schema,CN=Configuration,
 DC=coppersoftware,DC=COM
objectClass: classSchema
objectClassCategory: 0
possSuperiors: container
rDNAttID: cn
showInAdvancedViewOnly: TRUE
subClassOf: top
# Instruct Active Directory to reload the schema cache
# Do this if you need access to the class right away
DN:
changeType: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

Listing 9-10 ExtendSchema.ldf is an LDIF file that can be used with Ldifde to create a new attribute and a new class that uses the new attribute.

Before you can create a new class that includes a newly created attribute, the in-memory schema cache on the schema master server must be refreshed. Normally this happens every 5 minutes, but it can be forced by writing a value of 1 to the updateSchemaNow operational attribute of the server's RootDSE. The sample LDIF file in Listing 9-10 updates the schema cache after creating the attribute and again after creating the class. See the "Updating the Schema Cache" section later in this chapter for more information.

To use this LDIF file with Ldifde, you would enter a command similar to the following:

 ldifde -i -f authors.ldf -v 

LDIF files are convenient, particularly for bulk importing and exporting of directory data. However, for schema modifications, I prefer using programmatic means that can respond to errors and notify the user of problems.

Using a Program

Actually creating the new schema objects is just like creating any other object in the directory. I'll use the Create method of the IADsContainer interface, bound to the Schema container. Then I'll set the required attributes using the Put method of the IADs interface. Finally, I'll call the SetInfo method to update the directory server.

If any errors occur, they'll generally be at the SetInfo method. If sufficient access permission doesn't exist, an access denied error occurs. Frequently, a constraint violation occurs if invalid data is specified, such as requesting a nonexistent attribute name to be a required attribute for a new class.

The following #define statements are used by the code sample to hold the name and OID for the coppersoft-Authors-v3 attribute and the coppersoft-Book-v3 class. This way, they can be easily changed to include version numbers during the development and testing process. When the code is complete, remove the version numbers and add the objects to the schema and test again.

If you change the name of an attribute or class, the OID must also be changed. I use a method of keeping the last number of the OID in synch with the number at the end of the object's name.

 // Name and OID for new attribute and class #define AUTHORS_ATTR_OID
L"1.2.840.113556.1.4.7000.233.28688.28684.8.51404.1012371.312491.931313.3" #define AUTHORS_ATTR_NAME    L"coppersoft-Authors-v3" #define BOOK_CLASS_OID
L"1.2.840.113556.1.5.7000.111.28688.28684.8.204138.830347.950265.1272930.3" #define BOOK_CLASS_NAME    L"coppersoft-Book-v3"

Creating a New Attribute  When creating a new attribute for the schema, the following attributes must be set before calling SetInfo: lDAPDisplayName, oMSyntax, attributeID, attributeSyntax, isSingleValued, lDAPDisplayName, and oMSyntax. See Table 9-9 for more information about these mandatory attributeSchema attributes, as well as optional attributeSchema attributes.

Listing 9-11 shows the CreateDirectoryAttribute function, which creates the coppersoft-Authors attribute in the schema. Note that when the Create method is called, an IDispatch interface is returned to the new object. I use the QueryInterface method to request the IADs interface so that I can make calls to the Put method.

 //----------------------------------------------------------------
// Function:     CreateDirectoryAttribute
// Description:  Create new attribute object in the schema 
//
// In/Out:       IADsContainer* Pointer to schema container
// Returns:      HRESULT        COM/ADSI error code
//----------------------------------------------------------------
HRESULT CreateDirectoryAttribute ( IADsContainer *padsSchema )
{
HRESULT hResult;
// Verify that pointer is valid
if ( !padsSchema )
    return E_POINTER;
// Create new attributeSchema object in the schema container
IDispatch *piDisp = NULL;
hResult = padsSchema->Create ( L"attributeSchema",
                               _bstr_t( L"CN=" ) + 
                               _bstr_t( AUTHORS_ATTR_NAME ),
                               &piDisp );
if ( SUCCEEDED( hResult ) )
    {
    // Get IADs pointer to new object
    IADs *padsAttr = NULL;
    hResult = piDisp->QueryInterface ( IID_IADs,
                                       (void**) &padsAttr );
    if ( SUCCEEDED( hResult ) )
        {         //-------------------------------------------
        // Set the information for new attribute
        //-------------------------------------------
        // Indicate that this is a new attribute object
        hResult = padsAttr->Put ( L"objectClass",
            _variant_t( L"attributeSchema" ) );
        // Set the syntax to be a Unicode string
        hResult = padsAttr->Put ( L"attributeSyntax", 
            _variant_t( L"2.5.5.12" ) );
        // Set the XOM syntax for Unicode string
        hResult = padsAttr->Put ( L"oMSyntax", 
            _variant_t( L"64" ) );
        // Set the LDAP display name to match common name
        hResult = padsAttr->Put ( L"lDAPDisplayName",
            _variant_t( AUTHORS_ATTR_NAME ) );
        // Set the OID for this attribute
        hResult = padsAttr->Put ( L"attributeID", 
            _variant_t( AUTHORS_ATTR_OID ) );
        // Indicate that this value is multivalued
        hResult = padsAttr->Put ( L"isSingleValued",
            _variant_t ( false ) );
        // Update the server with this object
        hResult = padsAttr->SetInfo ();
        }
    // Release the attribute pointer
    if ( padsAttr )
        padsAttr->Release ();
    }
// Release the IDispatch pointer
if ( piDisp )
    piDisp->Release ();
return hResult;
}

Listing 9-11 The CreateDirectoryAttribute function from the ExtendSchema sample showing how to create a new attribute in the schema.

Creating a New Class  Creating a new class is similar to creating a new attribute. Listing 9-12 shows the CreateDirectoryClass function, which creates the coppersoft-Book class in the schema.

 //----------------------------------------------------------------
// Function:     CreateDirectoryClass
// Description:  Create new class object in schema
//
// In/Out:       IADsContainer*  Pointer to schema container
// Returns:      HRESULT         COM/ADSI error code
//----------------------------------------------------------------
HRESULT CreateDirectoryClass ( IADsContainer *padsSchema )
{
HRESULT hResult;
// Verify that pointer is valid
if ( !padsSchema )
    return E_POINTER;
// Create new classSchema object in the schema container
IDispatch *piDisp = NULL;
hResult = padsSchema->Create ( L"classSchema",
                               _bstr_t( L"CN=" ) +
                               _bstr_t( BOOK_CLASS_NAME ),
                               &piDisp );
if ( SUCCEEDED( hResult ) )
    {
    // Get IADs pointer to new object
    IADs *padsClass = NULL;
    hResult = piDisp->QueryInterface ( IID_IADs,
                                       (void**) &padsClass );
    if ( SUCCEEDED( hResult ) )
        {
        //-------------------------------------------
        // Set the information for new class
        //-------------------------------------------
        
        // Indicate that this is a structural class
        hResult = padsClass->Put ( L"objectClassCategory",
            _variant_t( (short) 0x01 ) );
        // Set the LDAP display name to match common name
        hResult = padsClass->Put ( L"lDAPDisplayName",
            _variant_t( BOOK_CLASS_NAME ) );
        // Set the Admin display name to match common name
        hResult = padsClass->Put ( L"adminDisplayName",
            _variant_t( BOOK_CLASS_NAME ) );
        // Set the description
        hResult = padsClass->Put ( L"description",
            _variant_t( L"Example class" ) );          // Set the OID for this class
        hResult = padsClass->Put ( L"governsID", 
            _variant_t( BOOK_CLASS_OID ) );
        // Indicate that this class inherits from the Top class
        hResult = padsClass->Put ( L"subClassOf", 
            _variant_t( L"top" ) );
        // Set the new authors attribute to be part of this class
        hResult = padsClass->Put ( L"mayContain", 
            _variant_t( AUTHORS_ATTR_NAME ) );
        // Update the server with this object
        hResult = padsClass->SetInfo();
        }
    // Release the class object pointer
    if ( padsClass )
        padsClass->Release ();
    }
// Release the IDispatch pointer
if ( piDisp )
    piDisp->Release ();
return hResult;
}

Listing 9-12 The CreateDirectoryClass function from the ExtendSchema sample showing how to create a new class in the schema.

Updating the Schema Cache

After creating the new schema objects, if the application needs to take advantage of them right away, you must direct it to request the schema master server to update its in-memory cache. For performance reasons, a version of the schema is kept in memory and is not automatically updated when new classes and attributes are added.

Eventually, the schema master updates the cache, usually within 5 minutes, but if you require access to your objects before then, it's simple to trigger an update. There are two methods you can use to trigger an update of the schema cache. The first method is to use the schemaUpdateNow constructed attribute on the server's RootDSE object. When this attribute is updated with any value, the server automatically flushes and refreshes its cache. The second method is to use the ADSystemInfo object supplied by ADSI. The RefreshSchemaCache method triggers a refresh of the cache on the local computer only. This method is useful if you are running your installation program on the schema master. Although, as I mentioned earlier, you should not change the schema master role just to accommodate your installation program.

Listing 9-13 shows the UpdateSchemaCache function, which can trigger an update to the schema cache by using schemaUpdateNow or RefreshSchemaCache:

 //----------------------------------------------------------------
// Function:     UpdateSchemaCache
// Description:  Triggers an update to the in-memory cache on the 
//               schema master
//
// In:           _bstr_t Computer name (NetBIOS or FQDN address)
//               bool    True to update the cache on the local 
//                       computer, regardless of the name passed.
//                       False to update cache on computer named.
// Returns:      HRESULT COM/ADSI error code
// Notes:        If program is running on schema master computer,
//               use bUpdateLocal = True to use ADSystemInfo
//               object to update local schema cache.
//               ADSystemInfo only supported on Windows 2000
//----------------------------------------------------------------
HRESULT UpdateSchemaCache( bstr_t bstrComputerName, bool bUpdateLocal )
{
HRESULT hResult;
// Do we update the local schema cache?
if ( bUpdateLocal )
    {
    // Create an ADSystemInfo object
    IADsADSystemInfo *padsADSystemInfo;
    hResult = CoCreateInstance( CLSID_ADSystemInfo,
                                NULL,
                                CLSCTX_INPROC_SERVER,
                                IID_IADsADSystemInfo,
                                (void**) &padsADSystemInfo );
    if ( SUCCEEDED( hResult ) )
        {
        // Call method to refresh cache
        hResult = padsADSystemInfo->RefreshSchemaCache ();
        }
    } else
    {
    // Nonlocal update
    // Create ADsPath to the schema NC on the schema master server
    _bstr_t strSchemaRootDSE = _bstr_t( "LDAP://" ) +
        bstrComputerName +
        _bstr_t( "/RootDSE" );
    // Bind to the rootDSE on the schema master
    IADs *padsSchemaRootDSE = NULL;
    hResult = ADsGetObject( strSchemaRootDSE,
                            IID_IADs,
                            (void**) &padsSchemaRootDSE );
    if ( SUCCEEDED( hResult ))
        {
        // Trigger the update by writing a value of 1
        hResult = padsSchemaRootDSE->Put ( L"schemaUpdateNow",
            _variant_t( true ) );
        // Won't update cache until written
        hResult = padsSchemaRootDSE->SetInfo();
        }
    // Free the RootDSE pointer
    if ( padsSchemaRootDSE )
        padsSchemaRootDSE->Release ();
    }
return hResult;
}

Listing 9-13 The UpdateSchemaCache function from the ExtendSchema sample showing how to trigger an update of the in-memory cache on the schema master.

Since updating the schema cache is time-consuming for the server, you should only do it when required. If you create several attributes, wait until all of them have been created before updating the cache. If you require access to the new class immediately, update the cache again after all the classes have been created.

ExtendSchema Sample

The main function of the ExtendSchema sample uses the functions described throughout this section to modify the schema. It's very straightforward, calling the respective functions to enable schema updates and verify permissions. Then it calls the function to create a new attribute and then the one to create a class that uses the attribute. Finally, it cleans up, refreshes the schema cache, and exits, as shown in Listing 9-14.

 int _tmain( int /* argc */, _TCHAR /* **argv */, _TCHAR /* **envp */)
{
    // Initialize COM
    HRESULT hResult = CoInitialize ( NULL );
    //-------------------------------------------
    // Bind to schema
    //-------------------------------------------
    // Pointer to schema container on schema master
    IADs *padsSchema = NULL;
    // DNS name of schema master
    bstr_t bstrSchemaMaster;
    // Bind to schema and return IADs and name string
    hResult = BindToSchemaMaster( &padsSchema, &bstrSchemaMaster );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _T("Schema Master DNS address: %ls\n"),
            (wchar_t*)bstrSchemaMaster );
    //-------------------------------------------
    // Verify Correct Permissions
    //-------------------------------------------
    hResult = VerifySchemaPermissions( padsSchema );
    // S_FALSE means persmission not allowed
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );     if ( hResult == S_FALSE )
        _tprintf( _T("Schema Permissions not okay.\n") );
    else
        _tprintf( _T("Schema Permissions okay.\n") );
    //-------------------------------------------
    // Enable Schema Changes
    //-------------------------------------------
    DWORD dwEnableChanges = 1;
    hResult = EnableSchemaUpdates( bstrSchemaMaster, 
        &dwEnableChanges );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _T("Registry unlocked, was %d\n"), 
            dwEnableChanges );
    // The following functions need an IADsContainer interface
    IADsContainer *padsSchemaCont = NULL;
    hResult = padsSchema->QueryInterface ( IID_IADsContainer,
        (void**) &padsSchemaCont );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    //-------------------------------------------
    // Create new attribute in the schema
    //-------------------------------------------
    hResult = CreateDirectoryAttribute ( padsSchemaCont );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _T("Created Attribute.\n") );
    //—————————————————————
    // Must update cache after all attributes are
    // created so they can be used by new classes
    //—————————————————————
    hResult = UpdateSchemaCache ( bstrSchemaMaster, false );     // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _T("Schema Cache Updated.\n") );
    //-------------------------------------------
    // Create new class in the schema
    //-------------------------------------------
    hResult = CreateDirectoryClass ( padsSchemaCont );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _TEXT("Created Class.\n") );
    //-------------------------------------------
    // Update schema master's in-memory cache
    //-------------------------------------------
    hResult = UpdateSchemaCache ( bstrSchemaMaster, false );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _TEXT("Schema Cache Updated.\n") );
    //-------------------------------------------
    // Restore Original Registry value
    //-------------------------------------------
    hResult = EnableSchemaUpdates( bstrSchemaMaster, 
        &dwEnableChanges );
    // Display any error and exit
    if ( FAILED( hResult ) )
        DisplayErrorAndExit( hResult );
    else
        _tprintf( _TEXT("Registry restored, was %d\n"), 
        dwEnableChanges );
    //-------------------------------------------
    // Release objects and exit
    //-------------------------------------------
    // Release the schema container interface
    if ( padsSchemaCont )
        padsSchema->Release ();     // Release the schema pointer
    if ( padsSchema )
        padsSchema->Release ();
    // Uninitialize COM and exit
    CoUninitialize ();
    // Exit with any lingering hResult
    return hResult;
}

Listing 9-14 The main function from the ExtendSchema sample.

Listing 9-15 shows the DisplayErrorAndExit function. This function executes when an error occurs, displays a formatted error string, and exits the application.

 //----------------------------------------------------------------
// Function:     DisplayErrorAndExit
// Description:  Displays error message and exits application
//
// In/Out:       DWORD containing error code
// Returns:      Does not return
//----------------------------------------------------------------
void DisplayErrorAndExit ( DWORD dwError )
{
    // Create error string
    PTSTR pszErrorMessage = NULL;
    FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                   FORMAT_MESSAGE_FROM_SYSTEM | 
                   FORMAT_MESSAGE_IGNORE_INSERTS,
                   NULL,
                   dwError,
                   MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                   (PTSTR) &pszErrorMessage,
                   0,
                   NULL );
    // Print error
    _tprintf( pszErrorMessage );
    // Free the buffer.
    LocalFree( pszErrorMessage );
    // Close application and return error
    exit( dwError );
}

Listing 9-15 The DisplayErrorAndExit function from the ExtendSchema sample, which displays an error string and exits the application when an error occurs.



MicrosoftR WindowsR 2000 Active DirectoryT Programming
MicrosoftR WindowsR 2000 Active DirectoryT Programming
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 108

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net