Active Directory

Microsoft Active Directory SDK

Active Directory Start Page

Active Directory

Version 2.5

Purpose

Active Directory™ is the directory service used in Windows™ 2000 and is the foundation of Windows 2000 distributed networks. The Active Directory APIs provide access to the data stored in the directory.

Active Directory Service Interfaces (ADSI) are a set of COM interfaces used to access the capabilities of directory services from different network providers in a distributed computing environment, to present a single set of directory service interfaces for managing network resources. Administrators and developers can use ADSI services to enumerate and manage the resources in a directory service, no matter which network environment contains the resource.

ADSI makes it easier to perform common administrative tasks, such as adding new users, managing printers, and locating resources throughout the distributed computing environment.

Where Applicable

Network Administrators will use ADSI to automate common administrative tasks, such as adding users and groups, managing printers, and setting permissions on network resources.

Independent Software Vendors and end user developers will use ADSI to "directory enable" their products and applications. Services can publish themselves in a directory, clients can use the directory to find the services, and both can use the directory to find and manipulate other objects of interest. Because Active Directory Service Interfaces are independent of the underlying directory service(s), directory-enabled products and applications will operate successfully in multiple network and directory environments.

Developer Audience

You can write ADSI client applications in many languages. For the majority of administrative tasks, ADSI defines interfaces and objects accessible from Automation-compliant languages like Microsoft® Visual Basic®, Microsoft Visual Basic Scripting Edition (VBScript), and Java to the more performance and efficiency-conscious languages such as C and C++. A good foundation in COM programming is useful to the ADSI programmer.

Run-time Requirements

The Active Directory runs on Windows 2000. However, client applications may be written on and run on Windows 95, Windows 98, Windows NT 4.0, and Windows 2000. There are client components available for all these operating systems available on the MSDN web site. In addition, developers will want the ADSI SDK, also available on the MSDN web site.

See Also

The Component Object Model

COM Clients and Servers

Active Directory Programmer's Guide

ADSI Programmer's Guide

Overview

General information about ADSI.

Reference

Documentation of ADSI interfaces and methods.

Feedback

Make error reports and feature requests directly to Microsoft.

Built on Tuesday, May 11, 1999


Legal Information

Active Directory

This is a preliminary document and may be changed substantially prior to final commercial release. This document is provided for informational purposes only and Microsoft makes no warranties, either express or implied, in this document. Information in this document is subject to change without notice. The entire risk of the use or the results of the use of this document remains with the user. The example companies, organizations, products, people and events depicted herein are fictitious. Without limiting the rights under copyright, no association with any real company, organization, product, person or event is intended or should be inferred. Complying with all applicable copyright laws is the responsibility of the user. No part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means, (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation.

Microsoft may have patents, patent applications trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.

© 1997-1999 Microsoft Corporation. All rights reserved.

Microsoft, MS-DOS, Windows and Windows NT, ActiveX, JScript, Visual Basic, Visual C++, Win32, Win32s, and Visual J++, are either registered trademarks or trademarks of Microsoft Corporation in the U.S.A. and/or other countries.

Macintosh is a trademark of Apple Computer, Inc. used under license.

The names of actual companies and products mentioned herein may be the trademarks of their respective owners.


Active Directory Programmer's Guide

[This is preliminary documentation and subject to change.]

Writing Powerful Applications that Use Active Directory

If you are writing a distributed application, Active Directory™ in Microsoft® Windows® 2000 offers many compelling reasons for you to want to integrate with it. The intent of this programmer's guide is to give you the knowledge you need to best write to Active Directory.

This guide is the beginning of a series of guidelines and recommendations for writing applications that use Active Directory. New subjects will be added to the series on the following Microsoft web site:

http://msdn.microsoft.com/developer/windowsnt5/adsi/default.htm

Fundamental Directory Features

A directory service is a fundamental service for distributed applications. A directory service, at a minimum, must provide the following:

Location transparency
The ability to find information about a user, group, networked service, or resource, without knowing addressing information.
Information on people and services
The ability to store user, group, organization, and service information in a structured, hierarchical tree.
Rich query
The ability to locate objects of interest by querying for properties of the object.
High availability
The ability to locate a replica of the directory at a location that is maximally efficient for the read/write operations.
Microsoft Active Directory provides these capabilities and much, much more.

Advanced Features of Active Directory

Active Directory also provides the following:

Support for Internet standards
Active Directory global namespace roots in DNS, and then uses LDAP to access objects within the directory service data store.
Tightly integrated and flexible security
Advantages include:
  • Choice of authentication packages depending on your application needs. Kerberos, SSL, or a combination (for example, establish an SSL channel for encryption and then use Kerberos for authentication) can be used.
  • Central management of service and resource access by using Active Directory users and groups.
  • Delegation of administration so that central administrators can delegate administrative tasks such as password changing or specific object creation and deletion.
  • The same access control mechanisms that are used on the Windows NT/Windows 2000 file system are used for Active Directory. Thus, the same tools that manage access control on the file system work for Active Directory.
  • Comprehensive Public Key infrastructure. The Microsoft Certificate Server and Smart Card support are integrated with Active Directory to provide Smart Card logon and Certificate management.
Scriptable interfaces for easy access
The primary and recommended application programming interface (API) for Active Directory is Active Directory Service Interfaces (ADSI). ADSI enables access to Active Directory by exposing objects stored in the directory as COM objects. A directory object is manipulated using the methods on one or more COM interfaces. You can already get ADSI providers from Microsoft for Novell NetWare Directory Services (NDS) and NetWare 3, Windows NT®, the Lightweight Directory Access Protocol (LDAP), Exchange 5.5, and the Internet Information Server (IIS) metabase. ADSI can be used from any tools from Microsoft Office applications to C/C++. ADSI supports extensibility so that additional functionality can be added to a provider to support new properties and methods. ADSI has a very simple programming model. ADSI abstracts the data management overhead that is characteristic of non-COM interfaces such as the LDAP C APIs . Since ADSI is fully scriptable, it easy to develop rich Web applications. ADO and OLE DB are supported for querying. Several tools make it easy to create an ADO or OLE DB connection and get results. By supporting ADO and OLE DB, Active Directory is just another OLE DB data provider.
Directory enabled system services
The ZAW technologies are written to take advantage of Active Directory. By creating an MSI package and using the application deployment feature of Windows 2000, your client application can be easily deployed to many desktops.
Key application integration
Key distributed applications such as Exchange will be tightly integrated with Active Directory. By doing so, companies can reduce the number of directory services that need to be managed.
Rich and extensible schema
The schema defines what objects and properties can be written and read from a directory service. Active Directory's schema is very rich. Most of the objects and properties a service needs are probably already there. If not, a distributed application can extend the schema to support the application's additional requirements.


About this Guide

A lot of work has been put into providing a directory service that is fully ready for the Enterprise. The intent of this guide is to provide you with the knowledge you need to take advantage of Active Directory. This guide is broken down into the following sections:

Concepts
This is the minimum you should know about Active Directory concepts and features prior to beginning your development effort.
Guidelines
These are the "best practices" for performing different operations on Active Directory. The guidelines go from the simple to the more complex, including what you need to know about:
  • Binding to Active Directory
  • Reading and setting properties
  • Querying
  • Extending the schema
  • Extending the UI to support the extended schema
  • Security
  • Working with access control
  • User and group management
  • Service publication
  • Dealing with consistency
  • Design Considerations for Disconnected Operations
  • Writing an ADSI Extension
  • How to set and get user and application configuration using policy
  • Taking advantage of Active Directory to deploy your applications

Since the primary and recommended API for Active Directory is ADSI, the guidelines should be used in parallel with the ADSI SDK material. The guidelines do not detail the interfaces, rather the guidelines give detail on "best practices" as well as code samples for the more commonly used interfaces.


Active Directory Concepts

  • Active Directory Basics
  • Active Directory Core Concepts
  • Active Directory Architecture


Active Directory Basics

  • What is a Directory Service?
  • Why Have a Directory Service?
  • What is Active Directory?


What is a Directory Service?

A directory is an information source used to store information about interesting objects. A telephone directory stores information about telephone subscribers. In a file system, the directory stores information about files.

In a distributed computing system or a public computer network like the Internet, there are many interesting objects, such as printers, fax servers, applications, databases, and other users. Users want to find and use these objects. Administrators want to manage how these objects are used.

In this document the terms directory and directory service refer to the directories found in public and private networks. A directory service differs from a directory in that it is both the directory information source and the services making the information available and usable to the users.


Why Have a Directory Service?

A directory service is one of the most important components of an extended computer system. Users and administrators frequently do not know the exact name of the objects they are interested in. They may know one of more attributes of the objects and can query the directory to get a list of objects that match the attributes. For example, "Find all duplex printers in Building 26." A directory service allows a user to find any object given one of its attributes.

A directory service can:

  • Enforce security defined by administrators to keep information safe from intruders.
  • Distribute a directory across many computers in a network.
  • Replicate a directory to make it available to more users and resistant to failure.
  • Partition a directory into multiple stores to allow the storage of a very large numbers of objects.

A directory service is both a management tool and an end user tool. As the number of objects in a network grows, the directory service becomes essential. The directory service is the hub around which a large distributed system turns.


What is Active Directory?

Active Directory™ is the directory service included with Windows® 2000. It extends the features of previous Windows-based directory services and adds entirely new features. Active Directory is secure, distributed, partitioned, and replicated. It is designed to work well in any size installation, from a single server with a few hundred objects to thousands of servers and millions of objects. Active Directory adds many new features that make it easy to navigate and manage large amounts of information, generating savings for both administrators and end users.


Active Directory Core Concepts

Some concepts and terms that are used to describe Active Directory are new and some aren't. Unfortunately, some of the terms that have been around for a while are used to mean more than one particular thing. Before going on, it is important that you understand how the following concepts and terms are meant in the context of Active Directory.

  • Scope
  • Namespace
  • Object
  • Container
  • Tree
  • Name
  • Object Identity
  • Naming Contexts and Partitions
  • Domains
  • Domain Trees
  • Forests
  • Sites


Scope

The scope of Active Directory is large. It can include every single object (printer, file, or user), every server, and every domain in a single wide area network. It can also include several wide area networks combined. Some of the following terms apply to more than a single network, so it is important to keep in mind that Active Directory can scale from a single computer, to a single computer network, to many computer networks combined.


Namespace

Active Directory is primarily a namespace, as is any directory service. A telephone directory is a namespace. A namespace is any bounded area in which a given name can be resolved. Name resolution is the process of translating a name into some object or information that the name represents. A telephone book forms a namespace in which the names of telephone subscribers can be resolved to telephone numbers. The NTFS file system forms a namespace in which the name of a file can be resolved to the file itself.

Active Directory forms a namespace in which the name of an object in the directory can be resolved to the object itself.


Object

An object is a distinct, named set of attributes that represents something concrete, such as a user, a printer, or an application. The attributes hold data describing the thing that is identified by the directory object. Attributes of a user might include the user's given name, surname, and e-mail address.


Container

A container is like an object in that it has attributes and is part of Active Directory namespace. However, unlike an object, it does not represent something concrete. It is the container of a group of objects and other containers.


Tree

Tree is used throughout this document to describe a hierarchy of objects and containers. Endpoints on the tree are usually objects. Nodes or branches are containers.

A tree shows how objects are connected or the path from one object to another. A simple directory is a container. A computer network or domain is also a container.

A contiguous subtree is any unbroken path in the tree including all the members of any container in that path.


Name

A name is used to identify every object in Active Directory. There are two different kinds of names.

Distinguished Name

Every object in Active Directory has a distinguished name (DN). The distinguished name identifies the domain that holds the object as well as the complete path through the container hierarchy used to reach the object. A typical DN might be CN=David Johnson,OU=Engineering,DC=Microsoft,DC=COM. This DN identifies the "David Johnson" user object in the Microsoft.Com domain.

If you are familiar with X.500 naming, the "DC=" type might come as a surprise. The "DC=" naming was introduced within the IETF so that X.500 directories can "plug into" the DNS namespace. This is exactly what Active Directory has done. The root of the global namespace for Active Directory is the DNS namespace. Thus, DNS domain names merge within Active Directory naming scheme.

Active Directory domain names are DNS domain names. For example, "Microsoft.Com" is a valid DNS domain name and could also be the name of an Active Directory Domain. Tight DNS integration means Active Directory fits naturally into Internet and intranet environments. Clients find directory servers quickly and easily. An enterprise can connect Active Directory servers directly to the Internet to facilitate secure communications and electronic commerce with customers and partners.

Location Service

Active Directory servers publish their addresses such that clients can find them knowing only the domain name. Active Directory servers are published using the Service Resource Records (SRV RRs) in DNS. The SRV RR is a DNS record used to map the name of a service to the address of a server offering that service. The name of a SRV RR is in this form:

<service>.<protocol>.<domain>

Active Directory servers offer the LDAP service over the TCP protocol so that published names are "ldap.tcp.<domain>". Thus, the SRV RR for "Microsoft.Com" is "ldap.tcp.microsoft.com". Additional information on the SRV RR indicates the priority and weight for the server, allowing clients to choose the best server for their needs.

When an Active Directory server is installed, it uses Dynamic DNS to publish itself. Since TCP/IP addresses are subject to change over time, servers periodically check their registrations to make sure they are correct, updating them if necessary.

Dynamic DNS

Dynamic DNS is a recent addition to the DNS standard. Dynamic DNS defines a protocol for updating a DNS server with new or changed values dynamically. Prior to Dynamic DNS, administrators needed to manually configure the records stored by DNS servers.

Relative Distinguished Name

The Relative Distinguished Name (RDN) of an object is the part of the name that is an attribute of the object itself. In the preceding example, the RDN of the "James Smith"` user object is "CN=James Smith". The RDN of the parent object is "CN=Users".


Object Identity

In addition to its name, every object in Active Directory has a unique identity. Active Directory is identity-based. This means that objects are known internally by their identity, not their current name. Objects may be moved or renamed, but their identity never changes. The identity of an object is a Globally Unique Identifier (GUID), assigned by the Directory Service Agent (DSA) when the object is created. The identity GUID is stored in an attribute, objectGUID, that is present on every object. The objectGUID attribute is protected such that it cannot be altered or removed. When storing a reference to an Active Directory object in an external store (for example, an SQL Server database), the objectGUID should be used, because it will never change, unlike a distinguished name or RDN, which may be changed.


Naming Contexts and Partitions

Active Directory is made up of one or more naming contexts or partitions. A naming context is any contiguous subtree of the directory. Naming contexts are the unit of partitioning. Attributes are the smallest unit of replication.

In Active Directory, a single server always holds at least three naming contexts:

  • The schema.
  • The configuration (replication topology and related metadata).
  • One or more user naming contexts (subtrees containing the actual objects in the directory).


Domains

A domain is a single security boundary of a Windows NT computer network. For more information on Windows NT domains, see your Windows NT documentation. Active Directory is made up of one or more domains. On a standalone workstation, the domain is the computer itself. A domain can span more than one physical location. Every domain has its own security policies and security relationships with other domains. When multiple domains are connected by trust relationships and share a common schema, configuration, and global catalog, you have a domain tree. Multiple domain trees can be connected together into a forest. All the domains in a forest also share a common schema, configuration, and global catalog.


Domain Trees

A domain tree ("Tree") is comprised of several domains that share a common schema and configuration, forming a contiguous namespace. Domains in a tree are also linked together by trust relationships. Active Directory is a set of one or more trees.

Trees can be viewed two ways. One view is the trust relationships between domains. The other view is the namespace of the domain tree.

Viewing Trust Relationships

You can draw a picture of a domain tree based on the individual domains and how they trust each other.

Windows NT establishes trust relationships between domains based on the Kerberos security protocol. Kerberos trust is transitive and hierarchical—if domain A trusts domain B and domain B trusts domain C, domain A trusts domain C as well.

Viewing the Namespace

You can also draw a picture of a domain tree based on the namespace. You can determine an object's distinguished name by following the path up the domain tree's namespace. This view is useful for grouping objects together into a logical hierarchy. The chief advantage of a contiguous namespace is that a deep search from the root of the namespace will search the entire hierarchy.


Forests

A forest is a set of one or more trees that do not form a contiguous namespace. All trees in a forest share a common schema, configuration, and global catalog. All trees in a given forest trust each other according to transitive hierarchical Kerberos trust relationships. Unlike trees, a forest does not need a distinct name. A forest exists as a set of Cross-reference objects and Kerberos trust relationships known to the member trees. trees in a forest form a hierarchy for the purposes of Kerberos trust; the tree name of at the root of the trust tree can be used to refer to a given forest.


Sites

A site is a location in a network holding Active Directory servers. A site is defined as one or more well connected TCP/IP subnets. "Well connected" means that network connectivity is highly reliable and fast. Defining a site as a set of subnets allows administrators to quickly and easily configure Active Directory access and replication topology to take advantage of the physical network. When users log in, Active Directory clients find Active Directory servers in the same site as the user. Since machines in the same Site are close to each other in network terms, communication among machines in the site is reliable, fast, and efficient. Determining the local Site at login time is easy because the user's workstation already knows what TCP/IP subnet it is on, and subnets translate directly to Active Directory sites.


Active Directory Architecture

This short section introduces some of the primary architectural components of Active Directory.

  • Directory System Agent
  • Data Model
  • Schema
  • Security Model
  • Administration Model
  • Global Catalog
  • Active Directory Security


Directory System Agent

The directory system agent (DSA) is the process that provides access to the store. The store is the physical store of directory information located on a hard disk. In Active Directory, the DSA is part of the local system authority (LSA) subsystem in Windows NT®. Clients access the directory using one of the following mechanisms supported by the DSA:

  • LDAP clients connect to the DSA using the LDAP protocol. LDAP is an acronym for Lightweight Directory Access Protocol. Active Directory supports LDAP 3.0, defined by RFC 2251, and LDAP 2.0, defined by RFC 1777. Windows 2000 clients (and Windows 95 and Windows 98 clients) with Active Directory client components installed use LDAP 3.0 to connect to the DSA.
  • MAPI clients such as Microsoft® Exchange connect to the DSA using the MAPI remote procedure call interface.
  • Windows clients that use a previous version of Windows NT connect to the DSA using the Security Account Manager (SAM) interface.
  • Active Directory DSA's connect to each other to perform replication using a proprietary remote procedure call interface.


Data Model

Active Directory data model is derived from the X.500 data model. The directory holds objects that represent things of various sorts, described by attributes. The universe of objects that can be stored in the directory is defined in the schema. For each object class, the schema defines what attributes an instance of the class must have, what additional attributes it may have, and what object class can be a parent of the current object class.


Schema

Active Directory schema is implemented as a set of object class instances stored in the directory. This is very different than many directories which have a schema but store it as a text file read at startup. Storing the schema in the directory has many advantages. For example, user applications can read it to discover what objects and properties are available.

Active Directory schema can be updated dynamically. That is, an application can extend the schema with new attributes and classes and use the extensions immediately. Schema updates are accomplished by creating or modifying the schema objects stored in the directory. Like every object in Active Directory, access-control lists (ACLs) protect schema objects, so only authorized users may alter the schema.


Security Model

The directory is part of the Windows NT trusted computing base and is a full participant in the Windows NT security infrastructure. Access-control lists (ACLs) protect all objects in Active Directory. Any attempt to access an object or attribute in Active Directory is validated against the ACL by the Windows NT access validation routines.


Administration Model

Authorized users perform administration in Active Directory. A user is authorized by a higher authority to perform a specified set of actions on a specified set of objects and object classes in some identified subtree of the directory. This is called delegated administration. Delegated administration allows very fine-grained control over who can do what and enables delegation of authority without granting elevated privileges.


Global Catalog

Active Directory can consist of many partitions or naming contexts. The distinguished name (DN) of an object includes enough information to locate a replica of the partition that holds the object. Many times, however, the user or application does not know the DN of the target object, or which partition might contain the object. The global catalog (GC) allows users and applications to find objects in an Active Directory domain tree given one or more attributes of the target object.

The global catalog contains a partial replica of every naming context in the directory. It contains the schema and configuration naming contexts as well. This means the GC holds a replica of every object in Active Directory but with only a small number of their attributes. The attributes in the GC are those most frequently used in search operations (such as a user's first and last names, login names, and so on) and those required to locate a full replica of the object. The GC allows users to quickly find objects of interest without knowing what domain holds them and without requiring a contiguous extended namespace in the enterprise.

The global catalog is built automatically by Active Directory replication system. The replication topology for the global catalog is generated automatically. The properties replicated into the global catalog include a base set defined by Microsoft. Administrators can specify additional properties to meet the needs of their installation.


Active Directory Security

This is only an overview of security in Active Directory. The Windows 2000 security model is described in more detail in a separate document:

http://premium.microsoft.com/msdn/library/conf/pdc97/distributed_security_services.htm

This section discusses the following:

  • Object and Attribute Protection
  • Delegation
  • Inheritance


Object and Attribute Protection

An access-control list (ACL) protects all objects in Active Directory.ACLs, determines who can see the object, what attributes they can see, and what actions each user can perform on the object. The existence of an object or an attribute is never revealed to a user who is not allowed to see it.

An ACL is a list of access-control entries (ACEs) stored with the object it protects. In Windows NT, an ACL is stored as a binary value, called a security descriptor. Each ACE contains a Security Identifier (SID), which identifies the principal (user or group) to whom the ACE applies, and information on what type of access the ACE grants or denies.

ACLs on directory objects contain ACEs that apply to the object as a whole and ACEs that apply to the individual attributes of the object. This allows an administrator to control not just which users can see an object, but what properties those users can see. For example, all users might be granted read access to the e-mail and telephone number attributes for all other users, but security properties of users might be denied to all but members of a special security administrators group. Individual users might be granted write access to personal attributes such as the telephone and mailing addresses on their own user objects.


Delegation

Delegation is one of the most important security features of Active Directory. Delegation allows a higher administrative authority to grant specific administrative rights for containers and subtrees to individuals and groups. This eliminates the need for "domain administrators" with sweeping authority over large segments of the user population.

ACEs can grant specific administrative rights on the objects in a container to a user or group. Rights are granted for specific operations on specific object classes using ACEs in the container's ACL. For example, to allow user "James Smith" to be an administrator of the "Corporate Accounting" organizational unit, you would add ACEs to the ACL on "Corporate Accounting" as follows:

"James Smith";Grant ;Create, Modify, Delete;Object-Class User
"James Smith";Grant ;Create, Modify, Delete;Object-Class Group
"James Smith";Grant ;Write;Object-Class User; Attribute Password

Now James Smith can create new users and groups in Corporate Accounting and set the passwords on existing users, but he cannot create any other object classes and he cannot affect users in any other containers (unless, of course, he is granted that access by ACEs on the other containers.


Inheritance

Inheritance allows a given ACE to be propagated from the container where it was applied to all children of the container. Inheritance can be combined with delegation to grant administrative rights to a whole subtree of the directory in a single operation.


Guidelines for Writing Directory-Enabled Applications

This section provides guidelines and recommended practices for writing applications that use or publish information in Active Directory™.

The primary and recommended application programming interface (API) for Active Directory is Active Directory Service Interfaces (ADSI).

Note Active Directory in Windows® 2000 is a directory service that is compliant with Lightweight Directory Access Protocol (LDAP) 3.0. LDAP is defined by RFC 2251 and others. Programmatic access to the directory using the LDAP API is also supported. This means you can write directory-enabled applications using the LDAP API. Microsoft provides an enhanced LDAP API for C/C++ programmers. Active Directory also supports other LDAP API implementations.

If you have existing applications that uses an implementation of the LDAP API, you can continue to use that API to access Active Directory. However, ADSI also provides a uniform, object-oriented directory access model to multiple directories. In addition to Windows 2000, ADSI supports directory access to Novell NetWare Directory Services (NDS), NetWare 3.11, and Windows NT® 4.0.

The ADSI programming model also allows other vendors to add support for other directories. These vendors can create new providers that expose their own namespace. The namespace can represent a directory service or anything else that the vendor wants. To implement a provider, you must follow some guidelines and implement a subset of ADSI COM interfaces depending on the degree of support that you want to expose. ADSI also provides a way for vendors to extend the ADSI interface using the ADSI extension mechanism. The vendors extend ADSI COM classes with their own methods and interfaces.

ADSI 2.5 provides ADSI support for computers running Windows NT 4.0, Windows® 95, and Windows® 98. ADSI 2.5 will be released as part of Windows 2000. The latest version of ADSI can also be downloaded from http://www.microsoft.com/adsi. Note that ADSI 2.5 is required for programmatic access using ADSI to Active Directory from computers running a Windows NT 4.0, Windows 95, or a Windows 98 computer.

  • Binding
  • Searching Active Directory
  • Reading Properties for an Active Directory Object
  • Extending the User Interface for Directory Objects
  • Replication and Data Integrity
  • Controlling Access to Active Directory Objects
  • Extending the Schema
  • Managing Users
  • Managing Groups
  • Service Publication
  • Mutual Authentication Using Kerberos


Binding

Accessing Active Directory™ is simply a matter of finding a Windows® 2000 domain controller and binding to an object in the directory. Use the ADSI LDAP provider to access Active Directory in a Windows 2000 environment.

For Windows NT® 4.0, Microsoft® provides the WinNT provider for access to directory information such as users, user groups, computers, services, and other network objects in the Windows NT 4.0 environment.

Do not use the WinNT provider to access Active Directory in Windows 2000 — unless you want to use the limited functionality of the WinNT provider.

When you bind to a directory object using ADSI, the directory object is represented as a COM object. This way, the binding operations require you to specify the COM interface that you want to use to access the directory object. Note that all ADSI COM objects that represent directory objects have an IADs interface. A COM object representing a directory object will also have other interfaces available, depending on the type of directory object. For example, a user object has an IADsUser interface in addition to an IADs interface. As with all COM objects, you can call the QueryInterface method to get pointers to the other interfaces supported on an object.

To bind to an object on a directory server, use one of the following functions or methods:

  • ADsGetObject function (C/C++ only)
  • ADsOpenObject function (C/C++ only)
  • GetObject method (VB and VBScript only)
  • IADsOpenDSObject::OpenDSObject method (VB and VBScript)

In general, the Get operations use the currently logged on user's credentials and default binding options to bind, whereas the Open operations allow you to supply specific user credentials and explicit binding options.

You would use the Open operations in the following cases:

  • If your application needs to bind using a user context that is different from that of the user that is currently logged on
  • If your application needs to enforce secure authentication (your application requires binding with the specified account, or as the user that is logged on)
  • If your application needs encryption to protect the data exchange over the network between your application and the directory server
  • If your application needs to explicitly bypass authentication and bind as Everyone or Guest.

If your application is written in C/C++, use ADsGetObject or ADsOpenObject.

If your application is written in Visual Basic, use the GetObject function provided by COM or the OpenDSObject method of the IADsOpenDSObject.

All these operations require the ADsPath binding string. The binding string has the following form:

LDAP://hostname/ObjectName

or

GC://hostname/ObjectName

In this example, LDAP: specifies the LDAP provider, which is the provider for Active Directory. GC: uses the LDAP provider to bind to the global catalog service in order to execute fast queries.

The hostname is optional. Avoid specifying a specific machine to bind to.

The ObjectName represents a specific Active Directory object. The ObjectName can be a distinguished name, canonical name, or an object GUID.

Note that you can use an ADsPath of LDAP or GC to bind to the root of the namespace. When you bind to the root of the namespace, you get a pointer to a namespace object which contains no properties and contains the domain object for LDAP and a container object containing a partial replica of all domains in the forest for GC.

Remember the following key points when binding:

  • Active Directory uses the ADSI LDAP provider.
  • Use serverless binding and rootDSE to eliminate dependencies on hardcoded server and domain names. The rootDSE is the root of the directory information tree on a directory server. The rootDSE has properties that contain information about the directory server itself.
  • Bind to the global catalog service for fast search. The global catalog service contains selected properties for all objects within Active Directory tree or forest.
  • If you want to access objects within the directory in a manner that is protected from renaming or movement of an object, you need to access the object using the object's objectGUID.
  • When possible, bind using the security context of the currently logged on user.
  • Use the encryption option to use an SSL encrypted channel to communicate between your application and Active Directory.


Serverless Binding and RootDSE

You should never hardcode a server name. Additionally, you should avoid unnecessarily tying your binding to a single server under most circumstances. Active Directory supports serverless binding, which means you don't have to specify a server if you are accessing Active Directory of the logged-on user's domain. When processing your bind call, ADSI finds the "best" Windows NT Domain Controller in the domain of the user that is currently logged on.

In LDAP 3.0, rootDSE is defined as the root of the directory information tree on a directory server. The rootDSE is not part of any namespace. The purpose of the rootDSE is to provide information about the directory server.

ADsPath string for rootDSE:

LDAP://rootDSE

or

LDAP://servername/rootDSE

In this example, servername is the name of the server. If the servername is not specified, a domain controller in the domain in which the user is logged on is used. ADSI attempts to find a domain controller within the client computer's site (a site is usually defined as an IP subnet) and connect to that domain controller. If a domain controller cannot be accessed within the site, ADSI uses the first domain controller that can be found.

For an Active Directory server (a Windows NT domain controller), the rootDSE contains the following properties:

Property | Description
currentTime | Current time set on this directory server.
subschemaSubentry | Distinguished name for the subSchema object. The subSchema object contains properties that expose the supported attributes (in the attributeTypes property) and classes (in the objectClasses property). The subschemaSubentry property and subschema are defined in LDAP 3.0 (see RFC 2251).
dsServiceName | The distinguished name of the NTDS settings object for this directory server.
namingContexts | Multi-valued. DISTINGUISHED NAMEs for all naming contexts stored on this directory server. By default, a Windows 2000 domain controller contains at least three namespaces: Schema, Configuration, and one for the domain of which the server is a member.
defaultNamingContext | By default, the distinguished name for the domain of which this directory server is a member.
schemaNamingContext | Distinguished name for the schema container.
configurationNamingContext | Distinguished name for the configuration container.
RootDomainNamingContext | Distinguished name for the first domain in the forest that contains the domain of which this directory server is a member.
SupportedControl | Multi-valued. OIDs for extension controls supported by this directory server.
SupportedLDAPVersion | Multi-valued. LDAP versions (specified by major version number) supported by this directory server.
HighestCommittedUSN | Highest USN used on this directory server. Used by directory replication.
SupportedSASLMechanisms | Security mechanisms supported for SASL negotiation (see LDAP RFCs). By default, GSSAPI is supported.
DnsHostName | DNS address for this directory server.
LdapServiceName | Service Principal Name (SPN) for the LDAP server. Used for mutual authentication.
ServerName | Distinguished name for the server object for this directory server in the configuration container.

The rootDSE is a well-known and reliable location on every directory server to get distinguished names to the domain container, schema container, configuration container, and other information about the server and the contents of its directory information tree. These properties rarely change on a particular server. Your application can read these properties at startup and use them throughout the session.

In summary, your application should leverage the logged-on user's domain membership to locate a directory server using serverless binding and leverage rootDSE to get the distinguished names for namespaces and use those distinguished names to bind to those objects.


Example Code for Getting the Distinguished Name of the Domain

The following code fragments get the distinguished name of the domain where the user is currently logged in.

[Visual Basic]
Dim rootDSE as IADs
Dim sADsPath as String

On Error GoTo NonAD

Set rootDSE = GetObject("LDAP://rootDSE")
SADsPath = "LDAP://" & rootDSE.Get("defaultNamingContext")
'Clean up
Set rootDSE = Nothing
NonAD:
    'Notify couldn't bind to rootDSE
[C++]
IADs *pRootDSE;
IADs *pObject;
HRESULT hr;
VARIANT var;
LPOLESTR szDSPath = new OLECHAR[MAX_PATH];

hr = ADsGetObject(L"LDAP://rootDSE",
                  IID_IADs,
                  (void**)&pRootDSE);
if (SUCCEEDED(hr))
{
    hr = pRootDSE->Get(L"defaultNamingContext",&var);
    wcscpy(szDSPath,L"LDAP://");
    wcscat(szDSPath,var.bstrVal);
    hr = ADsGetObject(szDSPath, IID_IADs, (void**) &pObject);
}
    // Clean up
    pRootDSE->Release();
    pObject->Release();
    VariantClear(&var);


Example Code for Getting the Distinguished Name for the Naming Context

The following code fragments contain a function that returns the distinguished name of the specified naming context (defined in the enumerated type NAMING_CONTEXT) using serverless binding and rootDSE.

[Visual Basic]
Function GetNamingContext(ByVal sPropertyName As String) As String
    Dim IADsRootDSE As IADs
    GetNamingContext = ""
    Set IADsRootDSE = GetObject("LDAP://rootDSE")
    GetNamingContext = IADsRootDSE.Get(sPropertyName)
    Set IADsRootDSE = Nothing
End Function
[C++]
typedef enum
{
    NC_CURRENT_DOMAIN = 0x0,
    NC_ROOT_OF_DOMAIN_TREE,
    NC_SCHEMA,
    NC_CONFIG
} NAMING_CONTEXT;

HRESULT GetNamingContextDN(
          NAMING_CONTEXT namingcontext,
          LPOLESTR *ppDNString
          )
{
    LPOLESTR pszDSPath = L"LDAP://rootDSE";
    LPOLESTR pszProperty = NULL;
    HRESULT hr = S_OK;
    VARIANT var;
    IADs *pObj = NULL;

    //Get the rootDSE.
    //ADsGetObject uses the logged-on user context.
    hr = ADsGetObject(pszDSPath,
                      IID_IADs,
                      (void**)&pObj);
    if (SUCCEEDED(hr))
    {
        //String to specify property to get based on namingcontext.
        switch (namingcontext)
        {
            //Ask for DN of root of domain tree that contains the current domain.
            case NC_ROOT_OF_DOMAIN_TREE:
                pszProperty = L"rootDomainNamingContext";
                break;
            //Ask for DN of schema container in the current domain.
            case NC_SCHEMA:
                pszProperty = L"schemaNamingContext";
                break;
            //Ask for DN of config container in the current domain.
            case NC_CONFIG:
                pszProperty = L"configurationNamingContext";
                break;
            //Ask for DN for current domain.
            case NC_CURRENT_DOMAIN:
            default:
                pszProperty = L"defaultNamingContext";
                break;
        }
        //Get the DN from the specified property of rootDSE.
        if (pszProperty)
        {
            hr = pObj->Get(pszProperty,&var);
            if (SUCCEEDED(hr))
            {
                *ppDNString = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(var.bstrVal)+1));
                if (*ppDNString)
                    wcscpy(*ppDNString, var.bstrVal);
            }
        }
    }
    // Clean up
    if (pObj)
        pObj->Release();
    VariantClear(&var);
    return hr;
    //The caller needs to call CoTaskMemFree on ppDNString.
}


Binding to the Global Catalog

The global catalog is a namespace that contains directory information from all domains in a forest. The global catalog contains a partial replica of every domain directory. It contains an entry for every object in the enterprise forest, but does not contain all the properties of each object. Instead, it contains only the properties that are specified for inclusion in the global catalog.

The global catalog is kept on specific servers throughout the enterprise. Only domain controllers can serve as global catalog servers. Administrators indicate whether a given domain controller will hold a global catalog by using the Active Directory Sites and Services Manager.

When you bind to the global catalog with ADSI, use the GC: moniker.

There are two ways to bind to the global catalog:

  • Bind to the enterprise root object to search across all domains in the forest.
  • Bind to a specific object to search that object and its children. For example, if you bind to a domain that has two domains beneath it in a domain tree in the forest, you can search across those three domains. Note that the distinguished name for the object you want to bind to is exactly the same as the distinguished name used to bind to the LDAP: namespace. Recall that LDAP: is a full replica of a single domain and that GC: is a partial replica of all domains in the forest.

As with the LDAP: moniker, you can use serverless binding (recommended) or bind to a specific global catalog server.

To search the entire forest

  1. Bind to the root of the GC namespace (GC:).
  2. Enumerate the GC container. The GC container contains a single object that you can use to search the entire forest.
  3. Use the object in the container to perform the search. In C/C++, call QueryInterface to get an IDirectorySearch pointer on the object so that you can use the IDirectorySearch interface to perform the search. In Visual Basic, use the object returned from the enumeration in your ADO query.


Example Code for Searching a Forest

[Visual Basic]

The following code fragment binds to the root of the global catalog (GC:) and enumerates the single object (which is the root of the forest) so that it can be used to search the entire forest.

Set gc = GetObject("GC:")
For each child in gc
    Set entpr = child
Next
' Now entpr is an object that can used
' to search the entire forest.
[C++]

The following code fragment contains a function that returns an IDirectorySearch pointer that can be used to search the entire forest.

The function does a serverless bind to the root of GC:, enumerates the single item (which is the root of the forest and can be used to search the entire forest), calls QueryInterface to get an IDirectorySearch pointer to the object, and returns that pointer for use by the caller to search the forest.

HRESULT GetGC(IDirectorySearch **ppDS)
{
    HRESULT hr = E_FAIL;
    HRESULT hrGC = E_FAIL;
    //Set IDirectorySearch pointer to NULL.
    *ppDS = NULL;
    //Used for enumerating the GC: root
    IADsContainer *pContainer = NULL;
    IUnknown *pUnk = NULL;
    IEnumVARIANT *pEnum = NULL;
    VARIANT var;
    ULONG lFetch;
        IDispatch *pDisp = NULL;

    //Use serverless bind to global catalog
    hr = ADsOpenObject(L"GC:",
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IADsContainer,
                (void**)&pContainer);
    if (SUCCEEDED(hr))
    {
       hr = pContainer->get__NewEnum( &pUnk );
        if (SUCCEEDED(hr))
        {
            hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
            if (SUCCEEDED(hr))
            {
            // Now Enumerate--there should be only one item.
            hr = pEnum->Next( 1, &var, &lFetch );
            if (SUCCEEDED(hr))
            {
                while( hr == S_OK )
                {
                    if ( lFetch == 1 )
                    {
                        pDisp = V_DISPATCH(&var);
                        hrGC = pDisp->QueryInterface( IID_IDirectorySearch, (void**)ppDS);
                        if (pDisp)
                            (pDisp)->Release();
                    }
                    VariantClear(&var);
                    hr = pEnum->Next( 1, &var, &lFetch );
                }
                //S_FALSE means we successfully read all the rows.
                if (hr==S_FALSE)
                    hr = hrGC;
            }
        }
        if (pEnum)
            pEnum->Release();
    }
    if (pUnk)
        pUnk->Release();
    }
    if (pContainer)
    pContainer->Release();

    if (FAILED(hr))
    {
        if (*ppDS)
        {
            (*ppDS)->Release();
            *ppDS = NULL;
        }
    }

    return hr;
}


Using objectGUID to Bind to an Object

Since an object's distinguished name changes if the object is renamed or moved, the distinguished name is not a reliable identifier for an object. In Active Directory, an object's objectGUID property is never changed, even if the object is renamed or moved to different places. For more information on objectGUID and identifiers, see Object Names and Identities.

You can bind to an object directly using an object's GUID. Note that binding with the object's GUID is only supported in Active Directory (that is, the LDAP provider). The binding string format is the following:

LDAP://servername/<GUID=XXXXX>

In this example, servername is the name of the directory server and XXXXX is the string representation of hexidecimal value of the GUID. The servername is optional. Note that this is not the string produced by StringFromGUID2 function in the COM library. Use the IADs::get_GUID method to retrieve the bindable string form of the objectGUID.


Example Code for Using ObjectGUID to Bind to an Object

If your application stores or caches identifiers or references to objects stored in the directory, the objectGUID is the best identifier to keep because 1) it stays the same even if the object is renamed or moved and 2) you can bind easily to the object using its GUID. In addition, if the object is renamed or moved, the objectGUID provides a single identifier that you can use to easily find and identify the object — rather than forcing you to compose a query that has conditions for all properties that would identify that object.

[Visual Basic]
Dim myObject as IADs

Set myObject = GetObject("LDAP://<GUID=63560110f7e1d111a6bfaaaf842b9cfa>")
[C++]
IADs *pADs;
LPWSTR pszFilter = L"LDAP://dc08/<GUID=63560110f7e1d111a6bfaaaf842b9cfa>";
hr = ADsGetObject( pszFilter, IID_IADs, (void**)&pADs);


Reading an Object's objectGUID and Creating a String Representation of the GUID

The objectGUID property is stored in the directory as an octet string (an array of one-byte characters). Use the IADs::get_GUID method to retrieve the bindable string form of the objectGUID.


Example Code for Reading the objectGuid Attribute

The following code fragments show a function that reads the objectGUID attribute and returns a string representation of the GUID that can be used to bind to the object.

[Visual Basic]
Dim sADsPathObject As String
Dim sObjectGUID As String
Dim sBindByGuidStr As String

Dim IADsObject As IADs
On Error Resume Next

' Ask the user for an ADsPath to start
sADsPathObject = InputBox("This code binds to a directory object by ADsPath, retrieves the GUID, then rebinds by GUID." & vbCrLf & vbCrLf & "Specify the ADsPath of the object to bind to :")

If sADsPathObject = "" Then
    Exit Sub
End If

MsgBox "Binding to " & sADsPathObject

' Bind to initial object
Set IADsObject = GetObject(sADsPathObject)

If (Err.Number <> 0) Then
   MsgBox Err.Number, "on GetObject method"
   Exit Sub
End If

' Save the GUID of the object
sObjectGUID = IADsObject.Guid

MsgBox "The GUID for " & vbCrLf & vbCrLf & sADsPathObject & vbCrLf & vbCrLf & " is " & vbCrLf & vbCrLf & sObjectGUID

' Release the initial object
Set IADsObject = Nothing

' Build a string for Binding to the object by GUID
sBindByGuidStr = "LDAP://<GUID=" & sObjectGUID & ">"

' Bind BACK to the Same object using the GUID
Set IADsObject = GetObject("LDAP://<GUID=" & sObjectGUID & ">")

If (Err.Number <> 0) Then
   MsgBox Err.Number, "on GetObject method"
   Exit Sub
End If

MsgBox "Successfully RE bound to " & sADsPathObject & vbCrLf & vbCrLf & " using the path:" & vbCrLf & vbCrLf & sBindByGuidStr

' Release bind by GUID Object
Set IADsObject = Nothing
[C++]
#define INC_OLE2
#define UNICODE 1
#define _WIN32_DCOM

#include <windows.h>
#include <winuser.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

#include <winldap.h>
#include <activeds.h>
#include "ADSIhelpers.h"
#include <assert.h>

void main()
{
// Initialize COM
CoInitialize(0);

WCHAR pwszADsPathObject[1024];
WCHAR pwszObjectGUID[1024];
WCHAR pwszBindByGuidStr[1024];
HRESULT hr;

IADs * pIADsObject = NULL;
IADs * pIADsObjectByGuid = NULL;

// Ask the user for a ADsPath to start
_putws(L"This code binds to a directory object by ADsPath, retrieves the GUID,\n"
       L" then rebinds by GUID. \n\nSpecify the ADsPath of the object to bind to :\n");
_getws(pwszADsPathObject);

if (pwszADsPathObject[0] == NULL)
    return;

wprintf(L"\nBinding to %s\n",pwszADsPathObject);

// Bind to initial object
hr = ADsGetObject( pwszADsPathObject,IID_IADs, (void **)& pIADsObject);
if (SUCCEEDED(hr))
{
    BSTR bsGuid = NULL;
    hr = pIADsObject->get_GUID(&bsGuid);

    if (SUCCEEDED(hr))
    {
        wprintf(L"\n The GUID for\n\n%s\n\nis\n\n%s\n",pwszADsPathObject,bsGuid);

        // Build a string for Binding to the object by GUID
        wsprintf(pwszBindByGuidStr,L"LDAP://<GUID=%s>",bsGuid);

        // Bind BACK to the Same object using the GUID
        hr = ADsGetObject( pwszBindByGuidStr,IID_IADs, (void **)& pIADsObjectByGuid);

        if (SUCCEEDED(hr))
        {
            wprintf(L"\nSuccessfully RE bound to\n\n%s\n\nUsing the path:\n\n%s\n", pwszADsPathObject,pwszBindByGuidStr);

            // Release bind by GUID Object
            pIADsObjectByGuid->Release();
            pIADsObjectByGuid = NULL;
        }

        SysFreeString(bsGuid);
    }

    pIADsObject->Release();
    pIADsObject = NULL;
}

if (FAILED(hr))
    _putws(L"Failed");

}


Binding to Well-Known Objects Using WKGUID

The containers can have one or more sub-objects that are important but can be renamed or moved. It can be cumbersome to keep track of these sub-objects and build correct binding strings for them, especially if they are renamed or moved.

To support rename-safe binding to these objects within these containers, the Active Directory has two properties: wellKnownObjects and otherWellKnownObjects. These properties have an attribute syntax of SYNTAX_DISTNAME_BINARY. They allow multiple values and contain the GUID/DN tuples of well-known objects within the containers on which they are set. The Active Directory maintains the distinguished name portion of each wellKnownObjects and otherWellKnownObjects entry so that it contains the current distinguished name of the object originally specified when the entry was created.

The otherWellKnownObjects property can be set and used on any object.

The wellKnownObjects property is used on the domainDNS and configuration containers. The wellKnownObjects property is a system-only property and can only be modified by the operating system.

The domainDNS container has the following well-known objects:

  • Users
  • Computers
  • System
  • Domain Controllers
  • Infrastructure
  • Deleted Objects
  • Lost and Found

The configuration container has the following well-known object:

  • Deleted Objects

These objects are always in every domainDNS and configuration container in the Active Directory.

You can bind to a well-known object using the WKGUID binding format. Note that binding with WKGUID is only supported in Active Directory (that is, the LDAP provider).

Important Always use the WKGUID binding format to bind to the well-known objects (listed above) in the domain and configuration containers. This ensures that you will be able to bind to those containers even if they are moved or renamed.

The WKGUID binding string format is the following:

LDAP://servername/<WKGUID=XXXXX,ContainerDN>

In this example, servername is the name of the directory server. The servername is optional.

XXXXX is the string representation of the hexadecimal value of the GUID that represents the well-known object. Note that 1) XXXXX is not the string produced by the StringFromGUID2 function in the COM library and 2) XXXXX is not the object's objectGUID.

To bind to an object specified in otherWellKnownObjects in an object, specify XXXXX as the bindable string form of the well-known object GUID of the object. See "Creating a bindable string representation of a GUID." For more information on setting the otherWellKnownObjects property on objects, see "Using the otherWellKnownObjects Property."

To bind to an object specified in wellKnownObjects in domainDNS or configuration containers, specify XXXXX as one of the constants defined in ntdsapi.h:

GUID_USERS_CONTAINER_W

GUID_COMPUTRS_CONTAINER_W

GUID_SYSTEMS_CONTAINER_W

GUID_DOMAIN_CONTROLLERS_CONTAINER_W

GUID_INFRASTRUCTURE_CONTAINER_W

GUID_DELETED_OBJECTS_CONTAINER_W

GUID_LOSTANDFOUND_CONTAINER_W

For example, if you wanted to bind to the users container in a domain, you would specify GUID_USERS_CONTAINER_W as XXXXX.

ContainerDN is the distinguished name of the container object that has this object represented as a value in its wellKnownObjects property. For example, if you wanted to bind to the users container in a domain, you would specify the domain's distinguished name as the ContainerDN.

For example, you would use the following binding to bind to the users container of the domain ArcadiaBay.com:

LDAP://<WKGUID=a9d1ca15768811d1aded00c04fd8d5cd,dc=arcadiabay,dc=com>


Example Code for Binding to the Users Container and Displaying the ADsPath

The following C++ program binds to the users container of the current user's domain and displays the users container's ADsPath (showing a live example of a WKGUID binding string). It also contains a function that binds to the specified well-known object (WKO) based on its WKO GUID in the current user's domain.

#include <wchar.h>
#include <objbase.h>

//For ADSI
#include <activeds.h>

//Make sure you define UNICODE
//Need to define version 5 for Windows 2000
#define _WIN32_WINNT 0x0500
#include <ntdsapi.h>

HRESULT GetWKDomainObject(LPOLESTR szBindableWKGUID, //IN. Bindable string GUID of well-known object.
                          IADs **ppObject //OUT. Return a pointer to the specified well-known object.
                          );

void wmain( int argc, wchar_t *argv[ ])
{
wprintf(L"This program finds the user's container in the current Window 2000 domain\n");
//Intialize COM
CoInitialize(NULL);
HRESULT hr = S_OK;
//Get rootDSE and the domain container's DN.
IADs *pObject = NULL;
hr = GetWKDomainObject(GUID_USERS_CONTAINER_W, //IN. Bindable string GUID of well-known object.
                          &pObject //OUT. Return a pointer to the specified well-known object.
                          );
if (FAILED(hr))
{
   wprintf(L"Not Found. Could not bind to the user container.\n");
   if (pObject)
     pObject->Release();
   return;
}

BSTR bstr;
pObject->get_ADsPath(&bstr);
wprintf (L"ADsPath of users Container: %s\n", bstr);
FreeADsStr(bstr);
if (pObject)
    pObject->Release();

//uninitialize COM
CoUninitialize();
return;

}

// This function gets the specified well-known object for the current user's domain.

HRESULT GetWKDomainObject(LPOLESTR szBindableWKGUID, //IN. Bindable string GUID of well-known object.
                          IADs **ppObject //OUT. Return a pointer to the specified well-known object.
                          )
{
HRESULT hr = E_FAIL;
//Get rootDSE and the domain container's DN.
IADs *pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);

//Get current domain DN.
if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"defaultNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build the WKGUID binding string.
        wcscpy(szPath,L"LDAP://");
        wcscat(szPath,L"<WKGUID=");
        wcscat(szPath,szBindableWKGUID);
        wcscat(szPath,L",");
        wcscat(szPath,var.bstrVal);
        wcscat(szPath,L">");
        //Print the binding string.
        //wprintf(L"WKGUID binding string: %s\n",szPath);
        VariantClear(&var);
        //Bind to the well-known object.
        hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)ppObject);
        if (FAILED(hr))
        {
            if (*ppObject)
            {
              (*ppObject)->Release();
              (*ppObject) = NULL;
            }
        }
    }
}
if (pObject)
  pObject->Release();

return hr;
}


Example Code for Creating a Bindable String Representation of a GUID

The following C++ code fragment shows a function that returns a string representation of a GUID that can be used to bind to the object:

HRESULT GUIDtoBindableString (LPGUID pGUID, LPOLESTR *ppGUIDString)
{
HRESULT hr = E_FAIL;
if (!pGUID)
  return E_INVALIDARG;
//Build bindable GUID string
LPOLESTR szDSGUID = new WCHAR [128];
DWORD dwLen =  sizeof(*pGUID);
LPBYTE lpByte = (LPBYTE) pGUID;
//Copy a blank string to make it a zero length string.
wcscpy( szDSGUID, L"" );
//Loop through to add each byte to the string.
for( DWORD dwItem = 0L; dwItem < dwLen ; dwItem++ )
{
  //Append to szDSGUID, double-byte, byte at dwItem index.
  swprintf(szDSGUID + wcslen(szDSGUID), L"%02x", lpByte[dwItem]);
  if( wcslen( szDSGUID ) > 128 )
    break;
}
//Allocate memory for string
*ppGUIDString = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szDSGUID)+1));
if (*ppGUIDString)
  wcscpy(*ppGUIDString, szDSGUID);
else
  hr=E_FAIL;
//Caller must free ppGUIDString using CoTaskMemFree.
return hr;
}


Enabling Rename-Safe Binding with the otherWellKnownObjects Property

If your application (or service) creates or uses containers in the directory and those containers hold objects that your application needs to bind to or search. You may want to take advantage of the otherWellKnownObjects property on those containers and the WKGUID binding feature.

Using otherWellKnownObjects property and WKGUID binding is beneficial in the following situation where rename-safe binding is required in relation to a specific container object:

  • You have a container object and it contains other important objects.
  • The important objects can be renamed or moved.
  • The important objects always exist for each instance of the container object. For example, every instance of a domainDNS object contains a users container (as well as others); therefore, the operating system has added a value to the wellKnownObjects property (note that wellKnownObjects is system only) that enables applications to bind in a rename-safe way to that container using its wellKnownObject GUID and the domainDNS container's distinguished name. The same thing can be done with any object using otherWellKnownObjects.
  • You need rename-safe binding and/or search capability on the important objects.

To add rename-safe binding and search capabilities

  1. Add a value to the otherWellKnownObjects property of the container object when the important object is created within that container. The value contains the GUID that represents the well-known object (note that this is not the objectGUID) and the distinguished name for that object.
  2. Use the WKGUID binding feature to bind to or search the important object.

The otherWellKnownObjects property allows multiple values and contains the GUID/DN tuples of well-known objects within the containers on which they are set. Values in the otherWellKnownObjects property are of type SYNTAX_DISTNAME_BINARY. They have the following form:

B:CharCount:WKOGUID:ObjectDN

In this example, CharCount is the count of hex digits in WKOGUID. This is 32 (number of hex digits in a GUID) for both otherWellKnownObjects and wellKnownObjects. WKOGUID is the hex digit representation of the well-known object GUID. ObjectDN is the distinguished name of the object represented by this WKO value. The Active Directory maintains the ObjectDN portion of each wellKnownObjects and otherWellKnownObjects value so that it contains the current distinguished name of the object originally specified when the value was created.

For example, you would use the following to represent an object with WKO GUID of {df447b5e-aa5b-11d2-8d53-00c04f79ab81} in the MyContainer object in the domain ArcadiaBay.com:

B:32:df447b5eaa5b11d28d5300c04f79ab81:cn=MyContainer,dc=arcadiabay,dc=com

To bind to this object, you would use the following WKGUID binding string:

LDAP://<WKGUID=df447b5eaa5b11d28d5300c04f79ab81,cn=MyContainer,dc=arcadiabay,dc=com>

After binding to this object, you can use the ADSI COM interfaces to search, read, modify, or delete the object.


Example Code for Creating a Container Object

The following program creates two objects: a container object and a sub-container object within it. A value for the sub-container object is added to the otherWellKnownObjects property of the container object. The program binds to the sub-container object using the WKGUID binding and displays its ADsPath. It then renames the sub-container object and binds again using the same WKGUID binding.

#include <wchar.h>
#include <objbase.h>
#include <activeds.h>

//Make sure you define UNICODE
//Need to define version 5 for Windows 2000
#define _WIN32_WINNT 0x0500
//FOR LDAP API...Required for Beta 3 only.
#include <winldap.h>
//Need to link against the following LIBs:
//wldap32.lib

static GUID MyWKOTestObjectGUID = { /* df447b5e-aa5b-11d2-8d53-00c04f79ab81 */
    0xdf447b5e,
    0xaa5b,
    0x11d2,
    {0x8d, 0x53, 0x00, 0xc0, 0x4f, 0x79, 0xab, 0x81}
  };

HRESULT GUIDtoBindableString (LPGUID pGUID, LPOLESTR *ppGUIDString);

HRESULT AddValueToOtherWKOProperty(LPOLESTR szContainerDN, //DN for container whose otherWellKnownObjects property to modify
                                   LPGUID pWKOGUID, //WKO GUID for the well-known object.
                                   LPOLESTR szWKOObjectDN //DN of the well-known object.
                                   );

void wmain( int argc, wchar_t *argv[ ])
{

LPOLESTR pszBuffer = new OLECHAR[MAX_PATH*2];
    wprintf(L"This program does the following:\n");
    wprintf(L"1. Creates a container (MyWKOTestContainer) in the current Window 2000 domain.\n");
    wprintf(L"2. Creates a container object (MyWKOTestObject) within the container.\n");
    wprintf(L"3. Adds a value for the container object on the otherWellKnownObject property of the container.\n");
    wprintf(L"4. Binds to the container object using WKGUID binding string.\n");
    wprintf(L"5. Renames the container object using WKGUID binding string.\n");
    wprintf(L"6. Binds to the container object using WKGUID binding string.\n");
    wprintf(L"7. Optionally, cleans up by removing the container and container object.\n\n");

//Intialize COM
CoInitialize(NULL);

HRESULT hr = S_OK;
IADs *pObject = NULL;
IADsContainer *pDomain = NULL;
IDispatch *pDisp = NULL;
IDispatch *pDispNewObject = NULL;
IADsContainer *pNewContainer = NULL;
IADs *pIADsObject = NULL;
IADs *pNewObject = NULL;
IADs *pTestWKO1 = NULL;
IADs *pTestWKO2 = NULL;
VARIANT vartest;
BSTR bstr;

LPOLESTR szNewContainerDN = new OLECHAR[MAX_PATH];
LPOLESTR szPath = new OLECHAR[MAX_PATH];
LPOLESTR szRelPath = new OLECHAR[MAX_PATH];
LPOLESTR szGUIDString = NULL;

//Names of the container and child object.
LPOLESTR szContainer = L"MyWKOTestContainer";
LPOLESTR szNewObject = L"MyWKOTestObject";

LPOLESTR szNewObjectRenameRDN = L"cn=ObjectwithNEWNAME";

//Get rootDSE and the domain container's DN.
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);
if (FAILED(hr))
{
   wprintf(L"Not Found. Could not bind to the domain.\n");
   if (pObject)
     pObject->Release();
   return;
}

hr = pObject->Get(L"defaultNamingContext",&var);
if (SUCCEEDED(hr))
{
    //Build the ADsPath to the domain
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    VariantClear(&var);
    //Bind to the current domain.
    hr = ADsOpenObject(szPath,
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADsContainer,
                     (void**)&pDomain);
    if (SUCCEEDED(hr))
    {
      //Create the container.
      wcscpy(szRelPath,L"cn=");
      wcscat(szRelPath,szContainer);
      hr = pDomain->Create(L"container", //ldapDisplayName of the class of the object to create.
                             szRelPath, //relative path in RDN=value format
                             &pDisp); //return an IDispatch pointer to the new object.
      if (SUCCEEDED(hr))
      {
        //QI for an IADs interface.
        hr = pDisp->QueryInterface(IID_IADs, (void **)&pIADsObject);
        //Commit the new object to the directory.
        hr = pIADsObject->SetInfo();
        //QI for an IADsContainer interface.
        hr = pDisp->QueryInterface(IID_IADsContainer, (void **)&pNewContainer);
        if (SUCCEEDED(hr))
        {
          //Create the new container object in the container.
          wcscpy(szRelPath,L"cn=");
          wcscat(szRelPath,szNewObject);
          hr = pNewContainer->Create(L"container", //ldapDisplayName of the class of the object to create.
                     szRelPath, //relative path in RDN=value format
                     &pDispNewObject); //return an IDispatch pointer to the new object.
          if (SUCCEEDED(hr))
          {
            //Get the DN of the new container object
            hr = pIADsObject->Get(L"distinguishedName", &var);
            if (SUCCEEDED(hr))
            {
              wcscpy(szNewContainerDN, var.bstrVal);
              VariantClear(&var);
              wprintf(L"Created new container with DN: %s\n",szNewContainerDN);
              hr = pDispNewObject->QueryInterface(IID_IADs, (void **)&pNewObject);
              if (SUCCEEDED(hr))
              {
                //Commit the new object to the directory.
                hr = pNewObject->SetInfo();
                //Get the DN for the new object
                hr = pNewObject->Get(L"distinguishedName", &var);
                if (SUCCEEDED(hr))
                {
                    wprintf(L"Created new child object with DN: %s\n",var.bstrVal);
                  //FOR BETA 3 only. Need to use LDAP API to set the otherWellKnownObjects property.
                  wprintf(L"Call AddValueToOtherWKOProperty with:\n");
                  wprintf(L"szContainer DN: %s\n",szNewContainerDN);
                  GUIDtoBindableString (&MyWKOTestObjectGUID, &szGUIDString);
                  wprintf(L"pWKOGUID (bindable string format): %s\n",szGUIDString);
                  wprintf(L"szWKOObjectDN: %s\n",var.bstrVal);
                  hr = AddValueToOtherWKOProperty(szNewContainerDN, //DN for container whose otherWellKnownObjects property to modify
                                   &MyWKOTestObjectGUID, //WKO GUID for the well-known object.
                                   var.bstrVal //DN of the well-known object.
                                   );
                  wprintf(L"AddValueToOtherWKOProperty returned: %x\n",hr);
                  if (SUCCEEDED(hr))
                  {
                      //Now bind using WKGUID binding
                      //Build the ADsPath to the well-known object
                      wcscpy(szPath,L"LDAP://<WKGUID=");
                      wcscat(szPath,szGUIDString);
                      wcscat(szPath,L",");
                      wcscat(szPath,szNewContainerDN);
                        wcscat(szPath,L">");
                      wprintf(L"Bind with the following WKGUID binding string: %s\n",szPath);
                      hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)&pTestWKO1);
                      if (SUCCEEDED(hr))
                      {
                          hr = pTestWKO1->Get(L"distinguishedName",&vartest);
                            if (SUCCEEDED(hr))
                          {
                              wprintf(L"Successfully bound to object. DN: %s\n",vartest.bstrVal);
                              VariantClear(&vartest);
                          }
                      }
                      else
                          wprintf(L"Binding failed with hr: %x\n",hr);

                      if (pTestWKO1)
                          pTestWKO1->Release();
                      //Bind again using the DN to get a regular ADsPath.
                      wcscpy(szPath,L"LDAP://");
                      wcscat(szPath,var.bstrVal);
                      hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)&pTestWKO1);
                      hr = pTestWKO1->get_ADsPath(&bstr);
                      //Rename the WKO object
                      hr = pNewContainer->MoveHere(bstr,szNewObjectRenameRDN,NULL);
                      FreeADsStr(bstr);
                      if (pTestWKO1)
                          pTestWKO1->Release();
                      //Now AGAIN bind using WKGUID binding
                      //Build the ADsPath to the well-known object
                      wcscpy(szPath,L"LDAP://<WKGUID=");
                      wcscat(szPath,szGUIDString);
                      wcscat(szPath,L",");
                      wcscat(szPath,szNewContainerDN);
                        wcscat(szPath,L">");
                      wprintf(L"Bind AGAIN with the following WKGUID binding string: %s\n",szPath);
                      hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)&pTestWKO2);
                      if (SUCCEEDED(hr))
                      {
                          hr = pTestWKO2->Get(L"distinguishedName",&vartest);
                            if (SUCCEEDED(hr))
                          {
                              wprintf(L"Successfully bound to object (Note the DN reflects the rename). DN: %s\n",vartest.bstrVal);
                              VariantClear(&vartest);
                          }
                      }
                      else
                          wprintf(L"Binding failed with hr: %x\n",hr);
                  }
                  CoTaskMemFree(szGUIDString);
                }
              }
              if (pNewObject)
                pNewObject->Release();
            }
            VariantClear(&var);
          }
          if (pIADsObject)
            pIADsObject->Release();
          if (pDispNewObject)
            pDispNewObject->Release();
        }

        //Ask user if they want us to delete the test containers.
        wprintf(L"Do you want to delete the test container and object (Y/N):");
        _getws(pszBuffer);
        if (0==wcsnicmp(L"Y", pszBuffer,1))
        {
            //Delete the object
            //Delete the container
            hr = pNewContainer->Delete(L"container",szNewObjectRenameRDN);
            if (SUCCEEDED(hr))
            {
              wprintf(L"Successfully deleted test object.\n");
              wcscpy(szRelPath,L"cn=");
              wcscat(szRelPath,szContainer);
              //Delete the container
              hr = pDomain->Delete(L"container",szRelPath);
              if (SUCCEEDED(hr))
                wprintf(L"Successfully deleted test container and its contents.\n");
              else
                wprintf(L"Failed to delete test container and its contents. hr: %x\n",hr);
            }
            else
              wprintf(L"Failed to delete test container and its contents. hr: %x\n",hr);
        }

        if (pNewContainer)
          pNewContainer->Release();
      }
      if (pDisp)
          pDisp->Release();
    }
    if (pDomain)
    pDomain->Release();
}
if (pObject)
   pObject->Release();

//Uninitialize COM
CoUninitialize();

    return;
}

HRESULT AddValueToOtherWKOProperty(LPOLESTR szContainerDN, //DN for container whose otherWellKnownObjects property to modify
                                   LPGUID pWKOGUID, //WKO GUID for the well-known object.
                                   LPOLESTR szWKOObjectDN //DN of the well-known object.
                                   )
{
HRESULT hr = E_FAIL;
LPOLESTR szGUIDString = new OLECHAR[MAX_PATH];
LPOLESTR szDNwithOctetString = new OLECHAR[MAX_PATH*2];
DWORD dwReturn;
//Connection handle
LDAP *hConnect = NULL;
//Specify NULL to bind to a DC in the current computer's domain.
//LDAP_PORT is the default port, 389
hConnect  = ldap_open(NULL,  LDAP_PORT);
//Bind using the preferred authentication method on Windows 2000
//and the logged on user's security context.
dwReturn = ldap_bind_s( hConnect, NULL, NULL, LDAP_AUTH_NEGOTIATE );
if (dwReturn==LDAP_SUCCESS)
{

  //Create the WKO value to add.
  GUIDtoBindableString (pWKOGUID, &szGUIDString);
  DWORD dwGUIDSize = (wcslen(szGUIDString));
  //Build the DNwithoctetstring
  swprintf(szDNwithOctetString, L"B:%d:%s:%s", dwGUIDSize, szGUIDString,szWKOObjectDN);
//  ULONG ulBerSize = (wcslen(szDNwithOctetString));
  //Build the BerVal
//  PCHAR pByteVal = (PCHAR)szDNwithOctetString;
//  berval berWKO;
//  berWKO.bv_len = ulBerSize;
//  berWKO.bv_val = pByteVal;
  //Build the mod structure to add the value.
  LDAPMod ldWKO;
  //mod_values takes a NULL terminated array of WCHARs.
  //We're adding a single value.
  WCHAR *StrValues[] = {szDNwithOctetString , NULL };
  //Operation
  ldWKO.mod_op = LDAP_MOD_ADD;
  //Attribute
  ldWKO.mod_type = L"otherWellKnownObjects";
  //Value to set.
  ldWKO.mod_vals.modv_strvals = StrValues;
  //mods is a NULL terminated array of LDAPMod structures.
  //We're adding a single value.
  LDAPMod *pMod[] = {&ldWKO,NULL};

  //Modify the object specified by szContainerDN.
  dwReturn = ldap_modify_s(  hConnect,
                             szContainerDN,
                             pMod);
  CoTaskMemFree(szGUIDString);

  if (dwReturn==LDAP_SUCCESS)
      hr = S_OK;
}
return hr;
}

HRESULT GUIDtoBindableString (LPGUID pGUID, LPOLESTR *ppGUIDString)
{
HRESULT hr = E_FAIL;
if (!pGUID)
  return E_INVALIDARG;
//Build bindable GUID string
LPOLESTR szDSGUID = new WCHAR [128];
DWORD dwLen =  sizeof(*pGUID);
LPBYTE lpByte = (LPBYTE) pGUID;
//Copy a blank string to make it a zero length string.
wcscpy( szDSGUID, L"" );
//Loop through to add each byte to the string.
for( DWORD dwItem = 0L; dwItem < dwLen ; dwItem++ )
{
  //Append to szDSGUID, double-byte, byte at dwItem index.
  swprintf(szDSGUID + wcslen(szDSGUID), L"%02x", lpByte[dwItem]);
  if( wcslen( szDSGUID ) > 128 )
    break;
}
//Allocate memory for string
*ppGUIDString = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szDSGUID)+1));
if (*ppGUIDString)
  wcscpy(*ppGUIDString, szDSGUID);
else
  hr=E_FAIL;
//Caller must free ppGUIDString using CoTaskMemFree.
return hr;
}

// This function gets the specified well-known object for the current user's domain.

HRESULT GetWKOObject(LPOLESTR szBindableWKGUID, //IN. Bindable string GUID of well-known object.
                          IADs **ppObject //OUT. Return a pointer to the specified well-known object.
                          )
{
HRESULT hr = E_FAIL;
//Get rootDSE and the domain container's DN.
IADs *pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);

//Get current domain DN.
if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"defaultNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build the WKGUID binding string.
        wcscpy(szPath,L"LDAP://");
        wcscat(szPath,L"<WKGUID=");
        wcscat(szPath,szBindableWKGUID);
        wcscat(szPath,L",");
        wcscat(szPath,var.bstrVal);
        wcscat(szPath,L">");
        //Print the binding string.
        //wprintf(L"WKGUID binding string: %s\n",szPath);
        VariantClear(&var);
        //Bind to the well-known object.
        hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)ppObject);
        if (FAILED(hr))
        {
            if (*ppObject)
            {
              (*ppObject)->Release();
              (*ppObject) = NULL;
            }
        }
    }
}
if (pObject)
  pObject->Release();

return hr;
}


Authentication

Every object in Active Directory has a unique security descriptor that defines access permissions required to read or update the object or its individual properties. Access permissions are determined by rights granted to users' accounts or by group memberships.

When an application binds to an object in the directory, the access permissions that the application has to that object are based on the user context specified during the bind operation. For the binding functions and methods (ADsGetObject, ADsOpenObject, GetObject, IADsOpenDSObject::OpenDSObject), an application can use the current logged-on user's credentials, an explicit user account, or use an unauthenticated user context (Guest).


GetObject and ADsGetObject

GetObject or AdsGetObject provide Single Sign On with no encryption. The application need not provide any credential information when accessing Active Directory information. ADSI uses the context of the currently logged in user. However, if secure authentication fails, ADSI attempts a simple bind with a NULL username and NULL password. If the simple bind succeeds, the user context for the binding is Guest. A simple bind is clear-text authentication. Note that because no username or password is sent, this is not a security issue.

If the application is a service running in LocalSystem context, the credentials depend on whether the service is running a workstation/member server or the domain controller. If it is a workstation/member server, the credentials are the machine's credentials. If it is a domain controller, it is LocalSystem credentials. On a domain controller, LocalSystem credentials means that the service has System level credentials on Active Directory.


Example Code for Binding to an Object Using AdsGetObject

The following code fragment binds to an object with the name Bob in the ArcadiaBay.com domain using ADsGetObject, which uses the logged-on user context:

[Visual Basic]
Dim myUser as IADs
Set myUser = GetObject("LDAP://CN=Bob,DC=ArcadiaBay,DC=com")
[C++]
IADs *pObject;
HRESULT hr;
hr = ADsGetObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
        IID_IADs,
        (void**) &pObject);


ADsOpenObject and IADsOpenDSObject::OpenDSObject

The major advantages of using ADsOpenObject and IADsOpenDSObject::OpenDSObject are the following:

  • The ability to specify an alternate user name and password to authenticate to the directory.
  • The ability to use encryption to protect the data exchange over the network between your application and the directory server.

It is recommended that you use the logged on user's credentials whenever possible. However, if you need to supply alternate credentials, you can use these Open methods. If you choose to bind using alternate credentials, do not cache the password. You can use the same alternate credentials in multiple bind operations by specifying the user name and password for the first bind operation and then specify only the user name to make subsequent binds. The system sets up a session on the first call and uses the same session on subsequent bind calls as long as the following conditions are met:

  • You specify the same user name in each bind operation.
  • You use serverless binding or bind to the same server in each bind operation.
  • You keep the session open by holding on to an object reference from one of the bind operations. The session is closed when the last object reference is released.

ADsOpenObject and IADsOpenDSObject::OpenDSObject take advantage of the Windows NT Security Support Provider Interfaces (SSPI) to allow flexibility in authentication options. The major advantage of using these interfaces is to provide different types of authentication to Active Directory clients and to encrypt the session. Currently, ADSI does not allow certificates to be passed in. Therefore, you can use SSL for encryption and then Kerberos, NTLM, or simple authentication, depending on how the flags are set on the dwReserved parameter. Note that you cannot ask for a specific SSPI provider in ADSI, although you will always get the highest preference protocol. In the case of a Windows 2000 client binding to a Windows 2000 server, the protocol is Kerberos. For a Windows NT 4.0 client binding to a Windows 2000 server, the protocol is NTLM. Not allowing a certificate for authentication is fine in the case of a Web page, because authentication occurs prior to running the Web page.

Although Open operations allow you to specify a user and password, you should use the current logged on user context to bind and leverage that user context to control access to the directory. If you use ADsOpenObject or IADsOpenDSObject::OpenDSObject to bind to an object, specify NULL for both username and password to use the current logged-on user's credentials. When an application binds using a user's credentials, that application can access only the objects and object properties to which that particular user has access. If the user does not have read access to an object, the application never sees the object when accessing the object in that user's context.

Finally, if you want to bind with no authentication, you can use the ADS_NO_AUTHENTICATION flag. No authentication means that ADSI attempts to bind as an anonymous user to the target object and performs no authentication. This is equivalent to requesting anonymous binding in LDAP and means "Everyone" is the security context.


Example Code for Binding to an Object Using ADsOpenObject

The following code fragment binds to an object with the name Bob in the ArcadiaBay.com domain using the logged-on user context and secure authentication:

[Visual Basic]
Dim np as IADsOpenDSObject
Dim myUser as IADs

Set np = GetObject("LDAP:")
Set myUser = np.OpenDSObject("LDAP://CN=Bob,DC=ArcadiaBay,DC=com",vbNullString,vbNullString, ADS_SECURE_AUTHENTICATION)
[C++]
IADs *pObject;
HRESULT hr;
hr = ADsOpenObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
        NULL,
        NULL,
        ADS_SECURE_AUTHENTICATION,
        IID_IADs,
        (void**) &pObject);

Note that if you set no authentication flags (dwReserved set to 0L), ADSI performs a simple bind, which is sent as clear text. If you specify a user account and password with no authentication flags, they go over the network as clear text — you should not perform a simple bind specifying the user and password.


Binding with Encryption

If you want to use encryption to protect the data exchange over the network between your application and the directory server, you must bind using ADsOpenObject or IADsOpenDSObject::OpenDSObject. You have two options for starting a session that uses encryption.

Kerberos
Specify the ADS_USE_SEALING flag to encrypt the data using Kerberos. You can also add the ADS_USE_SIGNING flag to verify data integrity (that is, to check to ensure the data received is the same as the data sent). If you just specify ADS_USE_SEALING, you get ADS_USE_SIGNING as well. Both flags require Kerberos authentication, which works only under the following conditions:
  • The client must be on a Windows 2000 system, or on a Windows 9x or Windows NT 4.0 system with the DS Client installed.
  • The client computer must be a member of a Windows 2000 mixed mode or native mode domain.
  • The client must be logged on to the Windows 2000 domain, or to a domain trusted by a Windows 2000 domain.
  • You must use ADsOpenObject or IADsOpenDSObject::OpenDSObject with NULL credentials, that is, you cannot specify alternate credentials.
SSL
Specify the ADS_USE_SSL flag to encrypt the channel with SSL. If you specify both ADS_SECURE_AUTHENTICATION and ADS_USE_SSL, ADSI first opens an SSL channel and performs a simple bind using the specified user name and password or the current user context if both user name and password are set to NULL. If you specify only the ADS_USE_SSL flag, ADSI will open SSL port 636 and then do a simple binding over that SSL channel.

To use SSL-based encryption while communicating with Active Directory, Active Directory must have enabled Public Key Infrastructure (PKI). PKI can be enabled by setting up an enterprise certificate authority on one of the servers in Active Directory (including one of the Active Directory servers itself). Setting up an enterprise certificate authority causes an Active Directory server to get a server certificate which can then be used to do SSL-based encryption.

Note that GetObject or ADsGetObject do not use encryption, so the LDAP requests used by ADSI and the data returned from the directory server go across the network as clear text. For debugging purposes, you may want to turn encryption off so you can use Network Monitor to see the LDAP requests and returns going between the client and the directory server.


Fast Binding Option for Batch Write/Modify Operations

When you call GetObject, ADsGetObject, ADsOpenObject, or IADsOpenDSObject::OpenDSObject, ADSI creates a COM object that represents the directory object specified by the call. Normally, ADSI first retrieves the objectClass property so that ADSI can expose the COM interfaces that are appropriate for that class of object. For example, a user object would expose the IADsUser interface in addition to the base ADSI interfaces supported for all objects (such as IADs, IDirectorySearch, IDirectoryObject, and so on).

For a single operation, this should have no effect on performance. However, if you are doing batch operations that require hundreds or thousands of bindings over a slow connection and those operations are write methods in the base ADSI interfaces (such as IADs::Put and IDirectoryObject::SetObjectAttributes), you may want to trade off full object support (that is, the object-specific interfaces such as IADsUser on user objects) for faster binding. To do this, use the ADS_FAST_BIND flag for ADsOpenObject and IADsOpenDSObject::OpenDSObject.

The ADS_FAST_BIND flag has the following effects:

  • The bind operation uses ADsOpenObject or IADsOpenDSObject::OpenDSObject and goes to the directory server once instead of twice. ADSI does not read objectClass and, therefore, exposes only the base ADSI interfaces for the object.
  • Only the following ADSI interfaces are supported on the ADSI COM object:
    • IADs
    • IADsContainer
    • IDirectoryObject
    • IDirectorySearch
    • IADsPropertyList
    • IADsObjectOptions
    • ISupportErrorInfo
    • IDirectoryAttrMgmt
    • IGetAttributeSyntax
    • IADsDeleteOps
  • If you use IADsContainer::GetObject to bind to child objects, the object returned will also have ADS_FAST_BIND characteristics.
  • No existence check for the object specified by the binding string. This means that subsequent method calls will fail if the object does not exist. Therefore, you should use the ADS_FAST_BIND option when you know the object already exists (for example, directly after performing a query that returned the distinguished names of the objects you want to bind to).
  • ADSI extensions are exposed for objects of class top. Therefore, only the extensions for the base ADSI interfaces listed above are exposed.


Binding to an Object's Parent Container

In ADSI, a directory object is represented by an ADSI COM object and every ADSI COM object exposes an IADs interface. The IADs interface has the IADs::get_Parent method so that you can get the ADsPath for the parent of the directory object. You can use that ADsPath to bind to the parent object.


Example Code for Binding to the Parent of an Object

The following C++ code fragment contains a function that binds to the parent of an object:

HRESULT GetParentObject(IADs *pObject, //Pointer the object whose parent to bind to.
                        IADs **ppParent //Return a pointer to the parent object.
                        )
{
    if ((!pObject)||(!ppParent))
        return E_INVALIDARG;

    HRESULT hr = E_FAIL;
    BSTR bstr;
    hr = pObject->get_Parent(&bstr);
    if (SUCCEEDED(hr))
    {
        //Bind to the parent container.
        *ppParent = NULL;
        hr = ADsOpenObject(bstr,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IADs,
             (void**)ppParent);
        if(FAILED(hr))
        {
            if (!(*ppParent))
            {
              (*ppParent)->Release();
              (*ppParent) = NULL;
            }
        }
    }
    FreeADsStr(bstr);
    return hr;
}


Binding to Child Objects

In ADSI, a container object exposes an IADsContainer interface. The IADsContainer interface has the IADsContainer::GetObject method so that you can get bind directly to a child object. Note that the object returned by IADsContainer::GetObject has the same security context as the object on which the method was called. This means if you have pointer to a container object and you know the relative path to a child object, you can avoid having to pass credentials again when binding to the child object. This may be useful when your application is binding using alternate credentials (that is, not the logged on user context).

The IADsContainer::GetObject method enables you to specify the relative RDN path (that is, relative to the current object) and class name (optional) and get back an IDispatch pointer to the specified child object.

For example, if you had an IADsContainer pointer (pCont) to a domainDNS object (DC=arcadiabay,DC=com), you could call IADsContainer::GetObject to bind to the Administrator user in the Users container (cn=Administrator,cn=Users,DC=arcadiabay,DC=com):

hr = pCont->GetObject(L"user",L"cn=Administrator,cn=Users",&pDisp);

Note the relative path in RDN=value format and that the path is relative to object represented by IADsContainer.


Example Code for Binding to the Users Container

The following C++ code fragment contains a function that binds to the users container and then uses IADsContainer::GetObject to bind to the Administrator user object within the users container:

LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);

//Get current domain DN.
if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"defaultNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build the WKGUID binding string.
        wcscpy(szPath,L"LDAP://cn=Users,");
        wcscat(szPath,var.bstrVal);
        if (pObject)
          pObject->Release();
        hr = ADsOpenObject(szPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);
        if (SUCCEEDED(hr))
        {
            hr = pObject->QueryInterface(IID_IADsContainer,(void**)&pContainer);
            if (SUCCEEDED(hr))
            {
                hr = pContainer->GetObject(L"user",L"cn=Administrator",&pDisp);
                if (SUCCEEDED(hr))
                {
                  hr = pDisp->QueryInterface(IID_IADs,(void**)&pChild);
                  if (SUCCEEDED(hr))
                  {
                      hr = pChild->get_ADsPath(&bstr);
                      if (SUCCEEDED(hr))
                      {
                        wprintf (L"ADsPath of child object: %s\n", bstr);
                        FreeADsStr(bstr);
                      }
                  }
                  if (pChild)
                      pChild->Release();
                }
                else
                  wprintf(L"GetObject failed with hr: %x\n",hr);
                if (pDisp)
                    pDisp->Release();
            }
            if (pContainer)
                 pContainer->Release();
        }
        if (pObject)
            pObject->Release();
    }
    VariantClear(&var);
}


Binding to the Root Domain of a Domain Tree

In Windows 2000, a forest can have multiple domain trees. A domain tree is a hierarchy of trusted domains. The Binding to the Global Catalog section showed how to use the global catalog to bind to the root of the forest to search all domains. What if you want to search a particular domain tree? How do you find the root of a domain tree?

If you have the distinguished name (DN) of the root domain in a tree, you can bind to that domain and use it as the base distinguished name to search all domains in that domain tree.

You can find the root domain of a tree using the NetEnumerateTrustedDomainsEx function. The NetEnumerateTrustedDomainsEx function enables you to retrieve the trusted domains in a forest. The list of trusted domains is returned as an array of DS_TRUSTED_DOMAINS structures which contain information about each domain, its parent in the domain tree, whether it is the root of a domain tree, and so on. This information can be used to find the root domain of a domain tree. If DS_DOMAIN_TREE_ROOT flag is set in the Flags member of a domain's DS_TRUSTED_DOMAINS structure, that domain is the root of domain tree. You can iterate the array of DS_TRUSTED_DOMAINS structures to get the list of all domains that are the root domain of a domain tree. You can use the ParentIndex member to find the parent domain of a given domain in the domain tree. The ParentIndex member is the parent domain's index within the array of DS_TRUSTED_DOMAINS structures.


Example Code for Binding to the Root Domain of a Domain Tree

The following code fragment contains a function that returns the distinguished name of the domain that is the root of the domain tree containing the specified domain:

//FindDomainTreeRoot returns the DN of the root domain of the domain tree
//containing the specified domain. If the domain specified is NULL,
//it returns the root domain of the domain tree containing
//the current logged-on user's domain.
//NOTE: Caller must free the memory for ppRootDomainDN (that is, using CoTaskMemFree).

//Make sure you define _WIN32_WINNT to force LMAPI headers to UNICODE.
//Need to define version 5 for Windows 2000 for SDDL.H
//#define _WIN32_WINNT 0x0500
//You need to include the following headers to use NetEnumerateTrustedDomainsEx
//and NetApiBufferFree
//#include <LMCONS.H>
//#include <dsgetdc.h>
//#include <lmapibuf.h>
//#include <LMErr.h>

HRESULT FindDomainTreeRoot(LPOLESTR szDomain, //Name of a domain in the domain tree.
                                              //Use NULL to use the current user's domain.
                       LPOLESTR *ppRootDomainDN,  //Return pointer to DN for root of the domain tree.
                       LPOLESTR szDCtoQuery) //Name of DC to query for this info.
                                             //Use NULL to use one in the current user's domain.
{
    HRESULT hr = E_FAIL;
    DWORD dwReturn;
    LPOLESTR szBuffer = new OLECHAR[MAX_PATH];
    LPOLESTR szSearchDomain = NULL;

    if (!ppRootDomainDN)
        return E_INVALIDARG;
    //If szDomain is NULL, use the current logged-on user's domain.
    if (!szDomain)
    {
        //Call NetWkstaUserGetInfo to get current logged on user info.
        PWKSTA_USER_INFO_1 pWkstaUserInfo = NULL;
        dwReturn = NetWkstaUserGetInfo(
                        NULL,
                        1, //For WKSTA_USER_INFO_1
                        (LPBYTE *)&pWkstaUserInfo
                        );

        if (dwReturn == NERR_Success)
        {
            //Get the logged-on user's domain.
            szSearchDomain = wcscpy(szBuffer,(pWkstaUserInfo->wkui1_logon_domain));
            wprintf(L"Current User's Domain: %s\n",szSearchDomain);
        }
        if (pWkstaUserInfo)
            NetApiBufferFree(pWkstaUserInfo);
    }
    else
        szSearchDomain = szDomain;

    if (!szSearchDomain)
        return hr;

    //Get the list of domains
    PDS_TRUSTED_DOMAINS pDomains = NULL;
    ULONG ulCount = 0L;
    dwReturn = NetEnumerateTrustedDomainsEx (
                                              szDCtoQuery, //IN LPWSTR ServerName OPTIONAL
                                              &pDomains,//OUT PDS_TRUSTED_DOMAINS *Domains
                                              &ulCount//OUT PULONG DomainCount
                                             );
    //Set to an out of range value
    ULONG ulParentIndex = ulCount+1;

    for (ULONG i = 0; i<ulCount; i++)
    {
//      wprintf(L"DomainIndex: %d\n", i);
      //Look for the specified domain in the trusted domain list.
      //Do a case-insensitive compare.
      if (0==wcsicmp(szSearchDomain, pDomains[i].NetbiosDomainName))
      {
          //Make sure it is in the forest
          if ((pDomains[i].Flags) & DS_DOMAIN_IN_FOREST)
          {
             // wprintf(L"DS_DOMAIN_IN_FOREST;");
             //If this is also the domaintree root, we're done.
             if ((pDomains[i].Flags) & DS_DOMAIN_TREE_ROOT)
             {
                //wprintf(L"DS_DOMAIN_TREE_ROOT;");
                ulParentIndex = i;
                hr = S_OK;
             }
             //Read ParentIndex member and then read the parent.
             else
             {
                 ulParentIndex = pDomains[i].ParentIndex;
                 //Recursively walk up the parent indexes to the tree root.
                 while (TRUE)
                 {
                     //Is this the tree root?
                     if ((pDomains[ulParentIndex].Flags) & DS_DOMAIN_TREE_ROOT)
                     {
                         hr = S_OK;
                         break;
                     }
                     //TODO: NEED to handle case of NT4 domains trusted and some 0 parentindex cases.
                     //Get the next parent index.
                     ulParentIndex = pDomains[ulParentIndex].ParentIndex;
                 }
             }
          }
          else
             wprintf(L"Domain %s is not in the forest.\n",szSearchDomain);
          break;
      }
    }
    if (SUCCEEDED(hr))
    {
        //Bind to the domain using the DNS name.
        IADs *pDomain = NULL;
        LPOLESTR szPath = new OLECHAR[MAX_PATH];
        VARIANT var;
        wcscpy(szPath,L"LDAP://");
        wcscat(szPath,pDomains[ulParentIndex].DnsDomainName);
        wcscat(szPath,L"/rootDSE");
        hr = ADsOpenObject(szPath,
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pDomain);
        if (SUCCEEDED(hr))
        {
          hr = pDomain->Get(L"defaultNamingContext",&var);
          //Allocate memory for string
          *ppRootDomainDN = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(var.bstrVal)+1));
          if (*ppRootDomainDN)
            wcscpy(*ppRootDomainDN, var.bstrVal);
        }
        VariantClear(&var);
        if (pDomain)
           pDomain->Release();
    }
    //Free the buffer
    NetApiBufferFree(pDomains);
    return hr;
}


Choosing an Interface

When you bind to an object, you can specify the ADSI COM interface whose pointer will be returned. An ADSI object can have a number of interfaces, depending on the class of object. However, every ADSI COM object has an IADs interface.

For Automation clients, use the IADs* interfaces. These interfaces are dual-interfaced, provide a greater level of abstraction, and provide data using VARIANTs and VARIANT arrays.

For C/C++ clients, you can use the IDirectorySearch interface for search operations and the IDirectoryObject interface to manipulate objects. These interfaces are not dual-interfaced. However, they do allow you to control exactly which attributes to retrieve (rather than returning the whole set) and allow access to the raw data stored in a property.

For example, security descriptors are stored as an octet string in an ntSecurityDescriptor property. When you use the Get method on an IADs interface to retrieve the ntSecurityDescriptor property, you get an IDispatch pointer to an IADsSecurityDescriptor object that wraps the security descriptor. At that point, you can use the QueryInterface method to query for the IADsSecurityDescriptor interface and use it's methods to manipulate the security descriptor. When you use the GetObjectAttributes method of an IDirectoryObject interface, you get a pointer to an array of bytes (LPBYTE), you can cast that pointer to PSECURITY_DESCRIPTOR flag and use the Win32 security APIs to manipulate the security descriptor.


Searching Active Directory

Searching is one of the key features of Active Directory™. A search operation enables you to find objects in Active Directory based on selection criteria (query) and to retrieve specified properties for the objects found.

Searching Active Directory is simply a matter of finding a Microsoft® Windows® 2000 domain controller, binding to the object where you want to begin your search in the directory, submitting a query, and processing the results.

The process for searching Active Directory includes the following steps:

  1. Decide what you want to find and where you want to find it.
  2. Choose the data access technology to use.
  3. Create a query filter (search criteria).
  4. Define the properties to return.
  5. Bind to the object where you want to start the search.
  6. Specify the depth of the search.
  7. Specify other search options.
  8. Execute the search and process the results.


Deciding What to Find

Before you search the directory, you need to figure out what you want to find. It sounds trivial, but what you want to find and what properties you want to return affects 1) where you bind to start your search, 2) the depth of your search, 3) your query filter, and 4) search performance.

For example, if you are searching for all user objects with surname Smith, you need to first decide where you want to search: a specific container or OU within a domain; a specific domain; a specific domain tree; or the entire forest. If you are looking for objects within a specific container or domain, you will get better performance by binding directly to that container or domain — instead of performing a subtree search on a domain tree.

Next, if you are checking for the existence of, or retrieving the properties of, a particular object that has a distinguished name you already know, you should do a base search, which searches only the object you have bound to. If you know an object is a direct descendant of a particular container, bind to that container and do a one-level search (attributeSchema and classSchema objects in the schema container and extended right objects in the extended rights container are good examples). If you don't know exactly where the object is, or if you want to search the object you've bound to and all the child objects below it in the directory hierarchy, do a subtree search.

Finally, if you are looking for a specific class of object, the query filter should have expressions that evaluate properties that are defined for that class. In addition, you should specify the objectCategory of the class of object you want to find as one of the expressions. You should use objectCategory instead of objectClass because objectCategory is indexed. Indexed attributes can increase the performance of the search. For example, to search for group objects, include the expression (objectCategory=group) in the filter. To search for user objects, you need to specify (&(objectClass=user)(objectCategory=person)) because 1) the computer class derives from the user class, so (objectClass=user) would return both users and computers and 2) both contact and user objects have an objectCategory of person, so (objectCategory=person) would return both users and contacts. For more information on indexed attributes, see Indexed Attributes.


Example Code for Searching for Users

The following programs search the current logged on user's domain for users.

[C++]

The following program searches the current logged on user's domain for all users (or a specific user based on a specified filter).

#include <objbase.h>
#include <wchar.h>
#include <activeds.h>
//Make sure you define UNICODE
//Need to define version 5 for Windows 2000
#define _WIN32_WINNT 0x0500
#include <sddl.h>

HRESULT FindUsers(IDirectorySearch *pContainerToSearch,  //IDirectorySearch pointer to the container to search.
         LPOLESTR szFilter, //Filter for finding specific users.
                            //NULL returns all user objects.
         LPOLESTR *pszPropertiesToReturn, //Properties to return for user objects found
                                          //NULL returns all set properties.
         BOOL bIsVerbose //TRUE means display all properties for the found objects.
                         //FALSE means only the RDN
                            );

void wmain( int argc, wchar_t *argv[ ])
{

//Handle the command line arguments.
LPOLESTR pszBuffer = new OLECHAR[MAX_PATH*2];
wcscpy(pszBuffer, L"");
BOOL bReturnVerbose = FALSE;

for (int i = 1;i<argc;i++)
{
  if (_wcsicmp(argv[i],L"/V") == 0)
  {
    bReturnVerbose = TRUE;
  }
  else if ((_wcsicmp(argv[i],L"/?") == 0)||
       (_wcsicmp(argv[i],L"-?") == 0))
  {
    wprintf(L"This program queries for users in the current user's domain.\n");
    wprintf(L"Syntax: queryusers [/V][querystring]\n");
    wprintf(L"where /V specifies that all properties for the found users should be returned.\n");
    wprintf(L"querystring is the query criteria in ldap query format.\n");
    wprintf(L"Defaults: If no /V is specified, the query returns only the RDN and DN of the items found.\n");
    wprintf(L"If no querystring is specified, the query returns all users.\n");
    wprintf(L"Example: queryusers (sn=Smith)\n");
    wprintf(L"Returns all users with surname Smith.\n");
    return;
  }
  else
  {
    wcscpy(pszBuffer,argv[i]);
  }
}
if (_wcsicmp(pszBuffer,L"") == 0)
  wprintf(L"\nFinding all user objects...\n\n");
else
  wprintf(L"\nFinding user objects based on query: %s...\n\n", pszBuffer);

//Initialize COM
CoInitialize(NULL);
HRESULT hr = S_OK;
//Get rootDSE and the current user's domain container distinguished name.
IADs *pObject = NULL;
IDirectorySearch *pContainerToSearch = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
         NULL,
         NULL,
         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
         IID_IADs,
         (void**)&pObject);
if (FAILED(hr))
{
   wprintf(L"Could not execute query. Could not bind to LDAP://rootDSE.\n");
   if (pObject)
   pObject->Release();
   return;
}
if (SUCCEEDED(hr))
{
  hr = pObject->Get(L"defaultNamingContext",&var);
  if (SUCCEEDED(hr))
  {
    //Build path to the domain container.
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    hr = ADsOpenObject(szPath,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IDirectorySearch,
             (void**)&pContainerToSearch);

    if (SUCCEEDED(hr))
    {
      hr = FindUsers(pContainerToSearch, //IDirectorySearch pointer to domainDNS container.
           pszBuffer,
           NULL, //Return all properties
           bReturnVerbose
         );
      if (SUCCEEDED(hr))
      {
        if (S_FALSE==hr)
           wprintf(L"No user object could be found.\n");
      }
      else if (0x8007203e==hr)
        wprintf(L"Could not execute query. An invalid filter was specified.\n");
      else
        wprintf(L"Query failed to run. HRESULT: %x\n",hr);
    }
    else
    {
       wprintf(L"Could not execute query. Could not bind to the container.\n");
    }
    if (pContainerToSearch)
       pContainerToSearch->Release();
  }
  VariantClear(&var);
}
if (pObject)
  pObject->Release();

// Uninitialize COM
CoUninitialize();
return;
}

HRESULT FindUsers(IDirectorySearch *pContainerToSearch,  //IDirectorySearch pointer to the container to search.
     LPOLESTR szFilter, //Filter for finding specific users.
                        //NULL returns all user objects.
     LPOLESTR *pszPropertiesToReturn, //Properties to return for user objects found
                        //NULL returns all set properties.
     BOOL bIsVerbose    //TRUE means all properties for the found objects are displayed.
                        //FALSE means only the RDN
              )
{
  if (!pContainerToSearch)
    return E_POINTER;
  //Create search filter
  LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];

  //Add the filter.
  wsprintf(pszSearchFilter, L"(&(objectClass=user)(objectCategory=person)%s)",szFilter);

  //Specify subtree search
  ADS_SEARCHPREF_INFO SearchPrefs;
  SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
  SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
  SearchPrefs.vValue.Integer = ADS_SCOPE_SUBTREE;
  DWORD dwNumPrefs = 1;

  // COL for iterations
  LPOLESTR pszColumn = NULL;
  ADS_SEARCH_COLUMN col;
  HRESULT hr;

  // Interface Pointers
  IADs  *pObj = NULL;
  IADs  * pIADs = NULL;

  // Handle used for searching.
  ADS_SEARCH_HANDLE hSearch = NULL;

  // Set the search preference.
  hr = pContainerToSearch->SetSearchPreference( &SearchPrefs, dwNumPrefs);
  if (FAILED(hr))
    return hr;

  LPOLESTR pszBool = NULL;
  DWORD dwBool;
  PSID pObjectSID = NULL;
  LPOLESTR szSID = NULL;
  LPOLESTR szDSGUID = new WCHAR [39];
  LPGUID pObjectGUID = NULL;
  FILETIME filetime;
  SYSTEMTIME systemtime;
  DATE date;
  VARIANT varDate;
  LARGE_INTEGER liValue;
  LPOLESTR *pszPropertyList = NULL;
  LPOLESTR pszNonVerboseList[] = {L"name",L"distinguishedName"};

  LPOLESTR szName = new OLECHAR[MAX_PATH];
  LPOLESTR szDN = new OLECHAR[MAX_PATH];

  int iCount = 0;
  DWORD x = 0L;

  if (!bIsVerbose)
  {
     //Return non-verbose list properties only.
     hr = pContainerToSearch->ExecuteSearch(pszSearchFilter,
                  pszNonVerboseList,
                  sizeof(pszNonVerboseList)/sizeof(LPOLESTR),
                  &hSearch
                  );
  }
  else
  {
    if (!pszPropertiesToReturn)
    {
      //Return all properties.
      hr = pContainerToSearch->ExecuteSearch(pszSearchFilter,
                  NULL,
                  0L,
                  &hSearch
                  );
    }
    else
    {
      //Specified subset.
      pszPropertyList = pszPropertiesToReturn;
       //Return specified properties.
       hr = pContainerToSearch->ExecuteSearch(pszSearchFilter,
                  pszPropertyList,
                  sizeof(pszPropertyList)/sizeof(LPOLESTR),
                  &hSearch
                  );
    }
  }
   if ( SUCCEEDED(hr) )
  {
  // Call IDirectorySearch::GetNextRow() to retrieve the next row
  //of data.
    hr = pContainerToSearch->GetFirstRow( hSearch);
    if (SUCCEEDED(hr))
    {
    while( hr != S_ADS_NOMORE_ROWS )
    {
      //Keep track of count.
      iCount++;
      if (bIsVerbose)
        wprintf(L"----------------------------------\n");
      // Loop through the array of passed column names,
      // print the data for each column.

      while( pContainerToSearch->GetNextColumnName( hSearch, &pszColumn ) != S_ADS_NOMORE_COLUMNS )
      {
        hr = pContainerToSearch->GetColumn( hSearch, pszColumn, &col );
        if ( SUCCEEDED(hr) )
        {
          // Print the data for the column and free the column.
          if(bIsVerbose)
          {
          // Get the data for this column.
          wprintf(L"%s\n",col.pszAttrName);
          switch (col.dwADsType)
          {
            case ADSTYPE_DN_STRING:
              for (x = 0; x< col.dwNumValues; x++)
              {
                wprintf(L"  %s\r\n",col.pADsValues[x].DNString);
              }
              break;
            case ADSTYPE_CASE_EXACT_STRING:
            case ADSTYPE_CASE_IGNORE_STRING:
            case ADSTYPE_PRINTABLE_STRING:
            case ADSTYPE_NUMERIC_STRING:
            case ADSTYPE_TYPEDNAME:
            case ADSTYPE_FAXNUMBER:
            case ADSTYPE_PATH:
            case ADSTYPE_OBJECT_CLASS:
              for (x = 0; x< col.dwNumValues; x++)
              {
                wprintf(L"  %s\r\n",col.pADsValues[x].CaseIgnoreString);
              }
              break;
            case ADSTYPE_BOOLEAN:
              for (x = 0; x< col.dwNumValues; x++)
              {
                dwBool = col.pADsValues[x].Boolean;
                pszBool = dwBool ? L"TRUE" : L"FALSE";
                wprintf(L"  %s\r\n",pszBool);
              }
              break;
            case ADSTYPE_INTEGER:
              for (x = 0; x< col.dwNumValues; x++)
              {
                wprintf(L"  %d\r\n",col.pADsValues[x].Integer);
              }
              break;
            case ADSTYPE_OCTET_STRING:
              if ( _wcsicmp(col.pszAttrName,L"objectSID") == 0 )
              {
                for (x = 0; x< col.dwNumValues; x++)
                {
                  pObjectSID = (PSID)(col.pADsValues[x].OctetString.lpValue);
                  //Convert SID to string.
                  ConvertSidToStringSid(pObjectSID, &szSID);
                  wprintf(L"  %s\r\n",szSID);
                  LocalFree(szSID);
                }
              }
              else if ( (_wcsicmp(col.pszAttrName,L"objectGUID") == 0) )
              {
                for (x = 0; x< col.dwNumValues; x++)
                {
                //Cast to LPGUID
                pObjectGUID = (LPGUID)(col.pADsValues[x].OctetString.lpValue);
                //Convert GUID to string.
                ::StringFromGUID2(*pObjectGUID, szDSGUID, 39);
                //Print the GUID
                wprintf(L"  %s\r\n",szDSGUID);
                }
              }
              else
                wprintf(L"  Value of type Octet String. No Conversion.");
              break;
            case ADSTYPE_UTC_TIME:
              for (x = 0; x< col.dwNumValues; x++)
              {
              systemtime = col.pADsValues[x].UTCTime;
              if (SystemTimeToVariantTime(&systemtime,
                            &date) != 0)
              {
                //Pack in variant.vt.
                varDate.vt = VT_DATE;
                varDate.date = date;
                VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);
                wprintf(L"  %s\r\n",varDate.bstrVal);
                VariantClear(&varDate);
              }
              else
                wprintf(L"  Could not convert UTC-Time.\n",pszColumn);
              }
              break;
            case ADSTYPE_LARGE_INTEGER:
              for (x = 0; x< col.dwNumValues; x++)
              {
              liValue = col.pADsValues[x].LargeInteger;
              filetime.dwLowDateTime = liValue.LowPart;
              filetime.dwHighDateTime = liValue.HighPart;
              if((filetime.dwHighDateTime==0) && (filetime.dwLowDateTime==0))
                {
                wprintf(L"  No value set.\n");
                }
              else
              {
                //Check for properties of type LargeInteger that represent time.
                //If TRUE, then convert to variant time.
                if ((0==wcscmp(L"accountExpires", col.pszAttrName))|
                  (0==wcscmp(L"badPasswordTime", col.pszAttrName))||
                  (0==wcscmp(L"lastLogon", col.pszAttrName))||
                  (0==wcscmp(L"lastLogoff", col.pszAttrName))||
                  (0==wcscmp(L"lockoutTime", col.pszAttrName))||
                  (0==wcscmp(L"pwdLastSet", col.pszAttrName))
                   )
                {
                  //Handle special case for Never Expires where low part is -1
                  if (filetime.dwLowDateTime==-1)
                  {
                    wprintf(L"  Never Expires.\n");
                  }
                  else
                    {
                    if (FileTimeToLocalFileTime(&filetime, &filetime) != 0)
                      {
                      if (FileTimeToSystemTime(&filetime,
                                 &systemtime) != 0)
                        {
                        if (SystemTimeToVariantTime(&systemtime,
                                      &date) != 0)
                          {
                          //Pack in variant.vt.
                          varDate.vt = VT_DATE;
                          varDate.date = date;
                          VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);
                          wprintf(L"  %s\r\n",varDate.bstrVal);
                          VariantClear(&varDate);
                          }
                        else
                          {
                          wprintf(L"  FileTimeToVariantTime failed\n");
                          }
                        }
                      else
                        {
                        wprintf(L"  FileTimeToSystemTime failed\n");
                        }

                      }
                    else
                      {
                      wprintf(L"  FileTimeToLocalFileTime failed\n");
                      }
                    }
                  }
                else
                  {
                  //Print the LargeInteger.
                  wprintf(L"  high: %d low: %d\r\n",filetime.dwHighDateTime, filetime.dwLowDateTime);
                  }
                }
              }
              break;
            case ADSTYPE_NT_SECURITY_DESCRIPTOR:
              for (x = 0; x< col.dwNumValues; x++)
                {
                wprintf(L"  Security descriptor.\n");
                }
              break;
            default:
              wprintf(L"Unknown type %d.\n",col.dwADsType);
            }
          }
          else
          {
          //Verbose handles only the two single-valued attributes: cn and ldapdisplayname
          //so this is a special case.
          if (0==wcscmp(L"name", pszColumn))
          {
            wcscpy(szName,col.pADsValues->CaseIgnoreString);
          }
          if (0==wcscmp(L"distinguishedName", pszColumn))
            {
            wcscpy(szDN,col.pADsValues->CaseIgnoreString);
            }
          }
          pContainerToSearch->FreeColumn( &col );
        }
        FreeADsMem( pszColumn );
      }
       if (!bIsVerbose)
         wprintf(L"%s\n  DN: %s\n\n",szName,szDN);
       //Get the next row
       hr = pContainerToSearch->GetNextRow( hSearch);
    }

    }
    // Close the search handle to clean up
    pContainerToSearch->CloseSearchHandle(hSearch);
  }
  if (SUCCEEDED(hr) && 0==iCount)
    hr = S_FALSE;

  return hr;
}
[Visual Basic]

The following routine searches the current logged on user's domain for users with the specified surname and returns the name and distinguishedName attributes for the objects that are found.

This routine uses ADO to perform the search.

Dim Con As ADODB.Connection
Dim ocommand As ADODB.Command
Dim gc As IADs

On Error Resume Next
'Maximum number of items to list on a msgbox.
MAX_DISPLAY = 5

'Prompt for surname to search for.
strName = InputBox("This routine searches in the current domain for users with the specified surname." & vbCrLf & vbCrLf &"Specify the surname:")

If strName = "" Then
  msgbox "No surname was specified. The routine will search for all users."
End If

'Create ADO connection object for Active Directory
Set Con = CreateObject("ADODB.Connection")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CreateObject"
  End If
Con.Provider = "ADsDSOObject"
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Provider"
  End If
Con.Open "Active Directory Provider"
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Open"
  End If

'Create ADO command object for the connection.
Set ocommand = CreateObject("ADODB.Command")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CreateObject"
  End If
ocommand.ActiveConnection = Con
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Active Connection"
  End If

'Get the ADsPath for the domain to search.
Set root = GetObject("LDAP://rootDSE")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject for rootDSE"
  End If
sDomain = root.Get("defaultNamingContext")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Get on defaultNamingContext"
  End If
Set domain = GetObject("LDAP://" & sDomain)
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject for domain"
  End If

'Build the ADsPath element of the commandtext
sADsPath = "<" & domain.ADsPath & ">"

'Build the filter element of the commandtext
If (strName = "") Then
  sFilter = "(&(objectCategory=person)(objectClass=user))"
Else
  sFilter = "(&(objectCategory=person)(objectClass=user)(sn=" & strName & "))"
End If

'Build the returned attributes element of the commandtext.
sAttribsToReturn = "name,distinguishedName"

'Build the depth element of the commandtext.
sDepth = "subTree"

'Assemble the commandtext.
ocommand.CommandText = sADsPath & ";" & sFilter & ";" & sAttribsToReturn & ";" & sDepth
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CommandText"
  End If
'Display
show_items "CommandText: " & ocommand.CommandText, ""

'Execute the query.
Set rs = ocommand.Execute
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Execute"
  End If

strText = "Found " & rs.RecordCount & " Users in the domain:"
intNumDisplay = 0
intCount = 0

' Navigate the record set
rs.MoveFirst
While Not rs.EOF
    intCount = intCount + 1
    strText = strText & vbCrLf & intCount & ") "
    For i = 0 To rs.Fields.Count - 1
        If rs.Fields(i).Type = adVariant And Not (IsNull(rs.Fields(i).Value)) Then
          strText = strText & rs.Fields(i).Name & " = "
          For j = LBound(rs.Fields(i).Value) To UBound(rs.Fields(i).Value)
             strText = strText & rs.Fields(i).Value(j) & " "
          Next
        Else
          strText = strText & rs.Fields(i).Name & " = " & rs.Fields(i).Value & vbCrLf
        End If
    Next
    intNumDisplay = intNumDisplay + 1
    'Display in msgbox if there are MAX_DISPLAY items to display
    If intNumDisplay = MAX_DISPLAY Then
        Call show_items(strText, "Users in domain")
        strText = ""
        intNumDisplay = 0
    End If
    rs.MoveNext
Wend

show_items strText, "Users in domain"
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Search domain for users with Surname " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Where to Search

An object can be in one of the following naming contexts:

  • Domain. This contains most of the highly used objects such as users, contacts, groups, organizational units, computers, and so on. Most queries will search a domain.
  • Schema container. This contains the attributeSchema and classSchema objects that define the types of objects and properties that can exist in the directory.
  • Configuration container. This contains configuration information that is relevant to the entire forest, such as sites, display specifiers, extended rights, partitions, and so on.

The global catalog contains a partial replica of all objects in the directory. It includes partial replicas of all domains in the forest as well as partial replicas of the schema and configuration containers.


Searching Domain Contents

Before discussing where to bind to begin a search for objects in domains, you need to know a little about how information is stored in Active Directory™.

If you have a forest with more than one domain, Active Directory does not store all information about all objects (that is, all properties for every object) on a single domain controller — for performance, scalability, and reliability reasons. A domain controller holds all information about the domain that it is a member of (it has a full replica of the domain). But a domain controller does not hold complete information about any other domain.

So, if you bind to the domain object (with referral chasing turned off — referrals are discussed a little later), you can search for any object in that domain (and only that domain). The search can retrieve any property and can use a query filter containing any property.

In a forest, domains are arranged hierarchically as domain trees. A domain tree can be just a single domain or a domain with one or more child domains. These child domains, in turn, can have child domains beneath them and so on. A domain tree is also a contiguous namespace. A contiguous namespace means that the child domains are a continuation of the naming hierarchy. For example, a domain ArcadiaBay.com (or DC=ArcadiaBay,DC=COM) could have a child domain mydivision (mydivision.AcadiaBay.com or DC=mydivision,DC=ArcadiaBay,DC=COM) which in turn could have a child mydev (mydev.mydivision.AcadiaBay.com or DC=mydev,DC=mydivision,DC=ArcadiaBay,DC=COM).

So, if you bind to a domain object (with referral chasing turned on) for a domain within a domain tree, you will search that domain and the entire hierarchy below it. The search can retrieve any property and can use a query filter containing any property.

If a domain controller contains a full replica of only its own domain, how can you perform a subtree search on a domain tree? A domain holds references to its child domains. When a domain controller processes a subtree search request against its own domain, the domain controller searches that domain and then returns referrals to each of its child domains to the client. A referral is the way that a directory server communicates that it does not contain the information required to complete a request (such as a query) but has a reference to a server that may contain the required information. In the case of a subtree search of a domain tree, a referral is returned for each direct child domain so that the search can be continued at a domain controller in each child domain. If referral chasing is turned on, the LDAP client library (WLDAP32.DLL) uses those referrals to bind to a domain controller in each child domain and continue the search. If referral chasing is turned off, the LDAP client does not resolve the referrals and the search is complete. For information, see Referral Chasing.

A subtree search on a domain tree with referral chasing turned on can be time-consuming if there is a slow connection to the domain controllers for the child domains. If you want to search only a single domain, you should turn referral chasing off to avoid having to search the child domains unnecessarily.


Searching the Schema

To search for attributeSchema or classSchema objects, bind to the schema container on any domain controller. The schema container is a full replica of the schema and is available on all domain controllers. Note that the schema container's distinguished name should be retrieved from the schemaNamingContext property from rootDSE. For sample code that retrieves the DN of the schema container, see Example Code for Getting the Distinguished Name for the Naming Context in the Binding chapter.


Searching the Configuration Container

To search for objects in the configuration container, bind to the configuration container on any domain controller. The configuration container is a full replica and is available on all domain controllers. Note that the configuration container's distinguished name should be retrieved from the configurationNamingContext property from rootDSE. For sample code that retrieves the DN of the configuration container, see Example Code for Getting the Distinguished Name for the Naming Context in the Binding chapter.


Searching Global Catalog Contents

Active Directory also has a global catalog (GC). The global catalog contains a partial replica of all objects in the directory. It also contains partial replicas of the schema and configuration containers. One or more domain controllers in a domain can hold a copy of the global catalog. For information about binding to a global catalog, see Binding to the Global Catalog.

The GC holds a replica of every object in Active Directory but with only a small number of their attributes. The attributes in the GC are those most frequently used in search operations (such as a user's first and last names, login names, and so on). The GC attributes also include those required to locate a full replica of the object. The GC allows users to quickly find objects of interest without knowing what domain holds them and without requiring a contiguous extended namespace in the enterprise (that is, you can search the entire forest).

So, if you bind to an object in the GC, you will search that object and the entire hierarchy below it — without having to go to any other server. However, the search can only use a query filter containing properties in the GC and can retrieve only properties in the GC.

Searching the global catalog has the following benefits:

  • Global catalog enables you to search the entire forest or any part of the forest as well as the schema and configuration containers.
  • Global catalog enables you to perform a complete search on a single server (no referrals or referral chasing is required).

Searching the global catalog has the following disadvantages:

  • Global catalog contains a small subset of the properties on each object. If your query filter includes properties that are not in the GC, the query will evaluate the expressions containing those properties as FALSE. If you specify non-global catalog properties in the list of properties to return, those properties are not retrieved.
  • To search the global catalog, a domain controller containing a global catalog must be available. If one is not available, you will not be able to perform a GC search.
  • The global catalog is read-only. This means you cannot bind to an object in the GC to create, modify, or delete objects.


Choosing the Data Access Technology

You can use the following technologies to search Active Directory™:

IDirectorySearch
ADSI provides the IDirectorySearch interface to query Active Directory (as well as other directory services such as NDS) using LDAP. IDirectorySearch is a COM interface that returns richly typed data (Integer, Octet String, String, Security Descriptor, UTC-Time, Large Integer, Boolean, etc.).
OLE DB
OLE DB is a set of COM interfaces that provide applications with uniform access to data stored in diverse information sources, regardless of location or type. ADSI also provides an OLE DB provider for ADSI that enables applications to use OLE DB to access Active Directory. The ADSI OLE DB provider uses the IDirectorySearch interfaces to submit queries to Active Directory and to collect the results.
ADO and other OLE DB-based data access technologies
The ADSI OLE DB provider enables any data access technology based on OLE DB (such as ADO) to search Active Directory.
LDAP API
Windows 2000 domain controllers are directory servers that are compliant with LDAP version 3. The LDAP API is a C-style function library. Applications can use the LDAP API to search Active Directory.

Which one do you choose?

For Microsoft® Visual Basic® and Microsoft Visual Basic Scripting Edition (VBScript), ADO is recommended.

For C/C++, you can choose any of the technologies.

If your application extensively uses ADSI, it may be simpler to use IDirectorySearch. If you use IDirectoryObject to manage objects in Active Directory, use IDirectorySearch to make handling the properties returned from the search easier. IDirectorySearch uses the same ADSVALUE structures as IDirectoryObject to represent properties. In addition, IDirectorySearch is exposed on almost all ADSI COM objects. If you have a pointer to an ADSI COM object, you can simply call QueryInterface to get an IDirectorySearch pointer that you can use to perform a search starting at the directory object represented by the ADSI COM object.

If your application already uses OLE DB, ADO or LDAP API, you can continue to use those technologies to search Active Directory as well.

If your application needs to join data from Active Directory and a SQL Server 7 database, use OLE DB. By using OLE DB, your application can perform distributed queries that reference Active Directory and tables and rowsets from one or more Microsoft® SQL Server 7 databases.


Creating a Query Filter

A query filter is simply the act of telling Active Directory what you want to find — in LDAP query syntax. All the specified data access technologies listed in the Choosing the Data Access Technology topic support the LDAP query syntax.

The LDAP query syntax is the following:

<expression><expression>…

A filter can contain one or more expressions. An expression has the following form:

(<logicaloperator><comparison><comparison…>)

where <logicaloperator> is the following:

Logical operator | Meaning
| | OR
& | AND
! | NOT

and <comparison> is the following:

(<attribute><operator><value>)

where <attribute> is the lDAPDisplayName of the attribute to evaluate, <value> is the value to compare against, and <operator> is one of the following comparison operators:

Logical operator | Meaning
= | Equals
~= | Approximately equals
<= | Less than or equal to
>= | Greater than or equal to

In addition, depending on the attribute syntax, the <value> may contain the * wildcard. Note that a <value> containing only * checks for the existence of the <attribute>.


Finding Objects by Class

Usually, you will want to find a specific class of objects. For example, you may want to find computers with location equal to Building 26.

(&(objectCategory=computer)(location=Building 26))

Why wasn't objectClass used? You should not use objectClass without another comparison containing an indexed attribute. Index attributes help increase the efficiency of a query. The objectClass attribute is multi-valued and not indexed. To specify the type or class of an object, you should use objectCategory instead.

//Inefficient
(objectClass=computer)

//More Efficient
(objectCategory=computer)

Note that there are some cases where a combination of objectClass and objectCategory must be used. The user class and contact class should be specified in the following manner:

(&(objectClass=user)(objectCategory=person))

(&(objectClass=contact)(objectCategory=person))

Note that you could search for both users and contacts with the following:

(objectCategory=person)


Finding Objects by Name

Most objects have cn (Common-Name) as their naming attribute. However, a few have naming attributes that are not cn (such as domainDNS, which is DC, and organizationalUnit, which is OU). To avoid having to remember what the naming attribute is, use the name (RDN) attribute.


Example Code for Filtering Objects by Name

The following filter finds all objects:

(objectClass=*)

The following filter finds all computers:

(objectCategory=computer)

The following filter finds all users and groups (note how an expression can be contained by another expression):

(|(&(objectClass=user)(objectCategory=person))(objectCategory=group))

The following filter finds all computers with names that begin with leased or corp:

(&(objectCategory=computer)(|(name=leased*)(name=corp*)))


Finding a List of Attributes To Query

If you are searching for objects of a particular class, the comparisons in your search filter should specify attributes that actually exist on the objects of that class. To get the list attributes on an object of a particular class, bind to that class in the abstract schema and call both IADsClass::get_MandatoryProperties and IADsClass::get_OptionalProperties to get the list of all properties for that object. For more information, see Guidelines for Binding to the Schema in the Extending the Schema chapter.

In addition, all objects inherit from the top abstract class. Therefore, any attribute in top can exist (although it may not be set) on any object.

If you are searching the global catalog, you should make sure you specify only attributes that are in the global catalog. Attributes that are included in the global catalog have the isMemberOfPartialAttributeSet set to TRUE on their attributeSchema objects.

In the global catalog, a back link attribute can be queried only if both of the following conditions are met: 1) the attribute is marked for inclusion in the global catalog and 2) the corresponding forward link is also marked for inclusion in the global catalog. This applies to query filters as well as query results.

In addition, some attributes (mostly on the user object) are constructed. Query filters cannot contain constructed attributes. Constructed attributes cannot be evaluated in query filters; however, they can be returned in query results. This applies to all the naming contexts and the global catalog. Attributes that are constructed have ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED (0x00000004) in the systemFlags property on their attributeSchema objects.


Checking the Query Filter Syntax

The LDAP API provides a simple syntax checking function. Note that it only checks the syntax and not the existence of the properties specified in the filter.

The following function checks the syntax of the query filter and returns S_OK if the filter is valid or S_FALSE if it is not.

HRESULT CheckFilterSyntax(
    LPOLESTR szServer, // NULL binds to a DC in the current domain.
    LPOLESTR szFilter) // Filter to check.
{
HRESULT hr = S_OK;
DWORD dwReturn;
LDAP *hConnect = NULL;  //Connection handle

if (!szFilter)
  return E_POINTER;

// LDAP_PORT is the default port, 389

hConnect  = ldap_open(szServer,  LDAP_PORT);

// Bind using the preferred authentication method on Windows 2000
// and the logged on user's security context.

dwReturn = ldap_bind_s( hConnect, NULL, NULL, LDAP_AUTH_NEGOTIATE );
if (dwReturn==LDAP_SUCCESS) {
    dwReturn = ldap_check_filter(hConnect, szFilter);
    if (dwReturn==LDAP_SUCCESS)
        hr = S_OK;
    else
        hr = S_FALSE;
}

// Unbind to free the connection.

ldap_unbind( hConnect );

return hr;
}


How to Specify Comparison Values

Each attribute type has a syntax that determines the type of comparison values that you can specify in a search filter for that attribute.

The following sections describe what is required for each attribute syntax. For more information on attribute syntaxes, see Syntaxes in the Extending the Schema chapter.

Boolean
The value specified in a filter must be one of the following string values:

TRUE

FALSE

Examples of Boolean

Filter specifying showInAdvancedViewOnly set to TRUE:

(showInAdvancedViewOnly=TRUE)

Filter specifying showInAdvancedViewOnly set to FALSE:

(showInAdvancedViewOnly=FALSE)
Integer and Enumeration
The value specified in a filter must be a decimal Integer. Hexadecimal values must be converted to decimal.

Note that the LDAP matching rule controls can be used to perform bit-wise comparisons.

Matching rules have the following syntax:

attributename:ruleOID:=value

where attributename is the lDAPDisplayName of the attribute, ruleOID is the OID for the matching rule control, and value is the value you want to use for comparison.

Active Directory supports the following matching rules.
Matching Rule OID Description
1.2.840.113556.1.4.803 LDAP_MATCHING_RULE_BIT_AND

The matching rule is true only if all bits from the property match the value. This rule is like the bit-wise AND operator.
1.2.840.113556.1.4.804 LDAP_MATCHING_RULE_BIT_OR

The matching rule is true if any bits from the property match the value. This rule is like the bit-wise OR operator.

Examples of Integer and Enumeration

Filter specifying groupType of Universal group (ADS_GROUP_TYPE_UNIVERSAL_GROUP is 0x00000008).

(groupType=8)

Filter specifying groupType of security-enabled Universal group (ADS_GROUP_TYPE_SECURITY_ENABLED is 0x80000000). So, ADS_GROUP_TYPE_SECURITY_ENABLED | ADS_GROUP_TYPE_UNIVERSAL_GROUP is 0x80000008 and converted to decimal value is 2147483650.

(groupType=2147483650)

Bit-wise comparisons

Filter specifying groupType with the ADS_GROUP_TYPE_SECURITY_ENABLED bit set:

(groupType:1.2.840.113556.1.4.804:=2147483648) )

The following query string searches for Universal distribution groups (that is, Universal groups without ADS_GROUP_TYPE_SECURITY_ENABLED flag):

(&(objectCategory=group)((&(groupType:1.2.840.113556.1.4.804:=8)(!(groupType:1.2.840.113556.1.4.803:=2147483650)))))
OctetString
The value specified in a filter is the data to be found. The data must be represented as an encoded byte string where each byte is preceded by a \ character.

Use the ADsEncodeBinaryData function to create an encoded string representation of binary data.

Note that wildcards are allowed.

Example of OctetString

Filter containing encoded string for schemaIDGUID with GUID value (in StringFromGUID2 format) of {BF967ABA-0DE6-11D0-A285-00AA003049E2}:

(schemaidguid=\BAz\96\BF\E6\0D\D0\11\A2\85\00\AA\000I\E2)

Filter containing encoded string for objectGUID with GUID value (in StringFromGUID2 format) of {FB4B7B35-E0AF-11D2-868C-00C04F8607E2}:

(objectguid=5\7BK\FB\AF\E0\D2\11\86\8C\00\C0O\86\07\E2)

The following code fragment prints the encoded string for the GUID guidmyTestAttributeDNString:

static const GUID guidmyTestAttributeDNString =
  {
  /* 9cb304e4-d60d-11d2-81a7-00c04fb98c1a */
    0x9cb304e4,
    0xd60d,
    0x11d2,
    {0x81, 0xa7, 0x00, 0xc0, 0x4f, 0xb9, 0x8c, 0x1a}
  };

LPOLESTR szTestBuffer = NULL;
HRESULT hrTemp = E_FAIL;

hrTemp = ADsEncodeBinaryData((LPBYTE)&guidmyTestAttributeDNString,
                              sizeof(GUID), &szTestBuffer);
if (SUCCEEDED(hrTemp))
    wprintf(L"%s\n",szTestBuffer);

if(szTestBuffer)
    FreeADsMem(szTestBuffer);
Sid
The value specified in a filter is the encoded byte string representation of the SID. See the discussion of encoded byte strings in the preceding discussion of the OctetString syntax.

Example of SID

Filter containing encoded string for objectSid with SID string value of S-1-5-21-1935655697-308236825-1417001333:

(ObjectSid=\01\04\00\00\00\00\00\05\15\00\00\00\11\C3\5Fs\19R\5F\12u\B9uT)
DN
The entire distinguished name that you want to match must be supplied.

Wildcards are not allowed.

Note that the objectCategory property also allows you to specify the lDAPDisplayName of the class set on the property. See the example that follows.

Examples of DN

Filter specifying a member containing CN=TestUser,DC=ArcadiaBay,DC=COM:

(member=CN=TestUser,DC=ArcadiaBay,DC=COM)

Two equivalent filters specifying an objectCategory set to CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=ArcadiaBay,DC=COM:

(objectCategory=CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=ArcadiaBay,DC=COM)

(objectCategory=attributeSchema)
INTEGER8
The value specified in a filter must be a decimal Integer. Hexadecimal values must be converted to decimal.

Examples of Integers

Filter specifying a creationTime set to a FILETIME of 3/10/99 3:31:32 PM:

(creationTime=125655822921406250)

The following functions create an exact match (=) filter for a large integer attribute and verify the existence of the attribute in the schema and its syntax:

HRESULT CreateExactMatchFilterLargeInteger(
                 LPOLESTR szAttribute,
                 INT64 liValue,
                 LPOLESTR *pszFilter
                 )
{
HRESULT hr = E_FAIL;

if ((!szAttribute)||(!pszFilter))
    return E_POINTER;

// Check that attribute exists and has
// Integer8 (Large Integer) syntax.

hr = CheckAttribute(szAttribute, L"Integer8");
if (S_OK==hr) {
    LPOLESTR szTempFilter = new OLECHAR[MAX_PATH];
    swprintf(szTempFilter, L"%s=%I64d", szAttribute, liValue);

    // Allocate buffer for the filter string.
    // Caller must free the buffer using CoTaskMemFree.
    *pszFilter = (OLECHAR *)CoTaskMemAlloc (
                       sizeof(OLECHAR)*(wcslen(szTempFilter)+1));
    if (*pszFilter) {
        wcscpy(*pszFilter, szTempFilter);
        hr = S_OK;
    }
    else
        hr=E_FAIL;
}
return hr;
}

HRESULT CheckAttribute(
             LPOLESTR szAttribute,
             LPOLESTR szSyntax
            )
{
HRESULT hr = E_FAIL;
BSTR bstr;
IADsProperty *pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];

if ((!szAttribute)||(!szSyntax))
    return E_POINTER;

swprintf(szPath,L"LDAP://schema/%s",szAttribute);
hr = ADsOpenObject(szPath,
           NULL,
           NULL,
           ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
           IID_IADsProperty,
           (void**)&pObject);
if (SUCCEEDED(hr)) {
    hr = pObject->get_Syntax(&bstr);
    if (SUCCEEDED(hr)) {
        if (0==_wcsicmp(bstr, szSyntax))
            hr = S_OK;
        else
            hr = S_FALSE;
    }
    SysFreeString(bstr);
}
if (pObject)
    pObject->Release();
return hr;
}
PrintableString
Attributes that have these syntaxes are supposed to adhere to specific character sets (see Syntaxes in the Extending the Schema chapter). Currently, Active Directory does not enforce those character sets but it may enforce them in the future.

The value specified in a filter is a string. The comparison is case-sensitive.

(myAttribute=TheValue)
GeneralizedTime
The value specified in a filter is a string that represents the date in the following form:
YYYYMMDDHHMMSS.0Z

Z means no time differential. Note that Active Directory stores date/time as Greenwich mean time (GMT) time. If you specify a time with no time differential, you are specifying the time in GMT time.

If you are not in the GMT time zone, you can use a differential value (instead of specifying Z) to specify a time according to your time zone and then add the differential between your zone and GMT. The differential is based on the following: GMT=Local+differential.

To specify a differential, use the following format:

YYYYMMDDHHMMSS.0[+/-]HHMM

Examples of GeneralizedTime

Filter specifying a whenCreated time set to 3/23/99 8:52:58 PM:

(whenCreated=19990323205258.0Z)

Filter specifying a whenCreated time set to 3/23/99 8:52:58 PM New Zealand Standard Time (differential is 12 hours):

(whenCreated=19990323205258.0+1200)

Calculating Time Zone Differential

The following function returns the differential between the current local time zone and GMT. The value returned is a string in the following format:

[+/-]HHMM

For example, Pacific Standard Time would be -0800.

HRESULT GetLocalTimeZoneDifferential (
                     LPOLESTR *pszDifferential
                   )
{
HRESULT hr = E_FAIL;
DWORD dwReturn = 0L;
TIME_ZONE_INFORMATION timezoneinfo;
LONG lTimeDifferential = 0;
LONG lHours = 0;
LONG lMinutes = 0;
LPOLESTR szString = new OLECHAR[MAX_PATH];
dwReturn  = GetTimeZoneInformation(&timezoneinfo);
switch (dwReturn)
{
  case TIME_ZONE_ID_STANDARD:
    lTimeDifferential = timezoneinfo.Bias + timezoneinfo.StandardBias;
    //Bias is in minutes--calculate the hours for HHMM format.
    lHours = -(lTimeDifferential/60);
    //Bias is in minutes--calculate the minutes for HHMM format.
    lMinutes = lTimeDifferential%60L;
      swprintf(szString, L"%+03d%02d",lHours,lMinutes);
    hr = S_OK;
    break;
  case TIME_ZONE_ID_DAYLIGHT:
    lTimeDifferential = timezoneinfo.Bias + timezoneinfo.DaylightBias;
    //Bias is in minutes--calculate the hours for HHMM format.
    //Need to take the additive inverse.
    //Bias is based on GMT=Local+Bias.
    //We need a differential based on GMT=Local-Bias.
    lHours = -(lTimeDifferential/60);
    //Bias is in minutes--calculate the minutes for HHMM format.
    lMinutes = lTimeDifferential%60L;
      swprintf(szString, L"%+03d%02d",lHours,lMinutes);
    hr = S_OK;
    break;
  case TIME_ZONE_ID_INVALID:
  default:
    hr = E_FAIL;
    break;
}

if (SUCCEEDED(hr))
{
  *pszDifferential = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szString)+1));
  if (*pszDifferential)
  {
  wcscpy(*pszDifferential, szString);
  //Call must free using CoTaskMemFree
  hr = S_OK;
  }
  else
    hr=E_FAIL;
}

return hr;
}
UTCTime
The value specified in a filter is a string that represents the date in the following form:
YYMMDDHHMMSSZ

Z means no time differential. Note that Active Directory stores date/time as GMT time. If you specify a time with no time differential, you are specifying the time in GMT time.

Note that the seconds (SS) value is optional.

If you are not in the GMT time zone, you can use a differential value (instead of specifying Z) to specify a time according to your time zone and then add the differential between your zone and GMT. The differential is based on the following: GMT=Local+differential.

To specify a differential, use the following format:

YYMMDDHHMMSS[+/-]HHMM

Examples of UTCTime

Filter specifying a myTimeAttrib time set to 3/23/99 8:52:58 PM:

(myTimeAttrib=990323205258Z)

Filter specifying a myTimeAttrib time set to 3/23/99 8:52:58 PM without seconds specified:

(myTimeAttrib=9903232052Z)

Filter specifying a myTimeAttrib time set to 3/23/99 8:52:58 PM New Zealand Standard Time (differential is 12 hours). This is equivalent to 3/23/99 8:52:58 AM GMT.

(myTimeAttrib=990323205258+1200)
DirectoryString
The value specified in a filter is a string. DirectoryString can contain Unicode characters. The comparison is case-insensitive.
(myAttribute2=THEVALUE)
OID
The entire OID that you want to match must be supplied.

Wildcards are not allowed.

Note that the objectCategory property also allows you to specify the lDAPDisplayName of the class set on the property. See the example below.

Examples of OID

Filter specifying governsID for volume class:

(governsID=1.2.840.113556.1.5.36)

Two equivalent filters specifying systemMustContain property containing uNCName, which has an OID of 1.2.840.113556.1.4.137:

(SystemMustContain=uNCName)

(SystemMustContain=1.2.840.113556.1.4.137)
Other Syntaxes
The following syntaxes are evaluated in a filter in the same manner as an octet string. See the discussion of the OctetString syntax earlier in this topic.
  • ObjectSecurityDescriptor
  • AccessPointDN
  • PresentationAddresses
  • ReplicaLink
  • DNWithString
  • DNWithOctetString
  • ORName


Listing Properties to Retrieve for Each Object Found

If you are searching for objects of a particular class, it only makes sense if you retrieve attributes that actually exist on the objects of that class. Follow the same rules discussed in Finding a List of Attributes To Query. Note that constructed attributes can be retrieved in query — but constructed attributes cannot be used in filters.


Retrieving the objectClass Property

The objectClass property contains the class of which the object is an instance, as well as all classes from which that class is derived. For example, the user class inherits from top, person, and organizationalPerson; therefore, the objectClass property contains the names of those classes, as well as user. So, how do you find out what class the object is an instance of? The objectClass property is the only property with multiple values that has ordered values — the first value is the top of the class inheritance tree, which is the top class, and the last value is the most derived class, which is the class that the object is an instance of.


Example Code for Retrieving the objectClass Property

The following function takes a pointer to a column containing an objectClass property and returns the instantiated objectClass of the object:

HRESULT GetClass(ADS_SEARCH_COLUMN *pcol, LPOLESTR *ppClass)
{
  if (!pcol)
    return E_POINTER;

  HRESULT hr = E_FAIL;
  if (ppClass)
  {
    LPOLESTR szClass = new OLECHAR[MAX_PATH];
    wcscpy(szClass, L"");
    if ( _wcsicmp(pcol->pszAttrName,L"objectClass") == 0 )
    {
      for (DWORD x = 0; x< pcol->dwNumValues; x++)
      {
        wcscpy(szClass, pcol->pADsValues[x].CaseIgnoreString);
      }
    }
    if (0==wcscmp(L"", szClass))
    {
      hr = E_FAIL;
    }
    else
    {
      //Allocate memory for string.
      //Caller must free using CoTaskMemFree.
      *ppClass = (OLECHAR *)CoTaskMemAlloc (
                             sizeof(OLECHAR)*(wcslen(szClass)+1));
      if (*ppClass)
      {
        wcscpy(*ppClass, szClass);
        hr = S_OK;
      }
      else
      hr=E_FAIL;
    }
  }
  return hr;
}


Binding to a Search Start Point

You can bind to any object in any of the following areas:

  • Domain
  • Schema
  • Configuration

In addition, you can bind to the global catalog, which contains a partial replica of all domains, as well as partial replicas of the schema and configuration containers. For more information on when to search in the domain, schema, configuration, and global catalog, see Where to Search.

For information on binding to an object, domain, the root of domain tree, and the root of the global catalog, see the Binding chapter.


Specifying Other Search Options

Active Directory™ enables you to control how the search is performed:

  • Search Scope
  • Synchronous vs. Asynchronous
  • Paging
  • Result Caching
  • Sorting the Search Results
  • Referral Chasing
  • Size Limit
  • Server Time Limit
  • Client Time-Out
  • Returning Only Attribute Names

To set these options, call IDirectorySearch::SetSearchPreferences and specify the options as an array of ADS_SEARCHPREF_INFO structures.


Search Scope

You can specify the scope of a search as either a base, one-level, or subtree search. Use the ADS_SEARCHPREF_SEARCH_SCOPE flag with the values of the ADS_SCOPEENUM enumeration to specify the search scope.

  • Base. A base search limits the search to only the base object. The maximum number of objects returned is always one. This search is useful to verify the existence of an object for retrieving group membership. For example, if you have an object's distinguished name, and you need to verify the object's existence based on the path, you can use a one-level search. If the search fails, you can assume that the object may have been renamed or moved to a different location, or you were given the wrong information about the object. (Note that you should store the globally unique identifier (GUID) of the object instead of the distinguished name, if you wish to revisit an object. The GUID will always reference the same object, regardless of where the object is moved within the directory hierarchy.)
  • One-level. A one-level search is restricted to the immediate children of a base object, but excludes the base object itself. This setting can perform a targeted search for immediate child objects of a parent object. For example, consider a parent object P1 and its immediate children: C1, C2, and C3. A one-level search evaluates C1, C2, and C3 against the search criteria, but does not evaluate P1. You can use a one-level search to enumerate all children of an object. In fact, an IADsContainer enumeration translates to a one-level search.
  • Subtree. A subtree search (or a deep search) includes all child objects as well as the base object. You can request the LDAP provider to chase referrals to other LDAP directory services, including other Active Directory domains or forests.


Synchronous vs. Asynchronous

When you perform a search using IDirectorySearch, the IDirectorySearch::ExecuteSearch method does not send the search request to the server — this method only saves the search parameters. The search request is sent when you call IDirectorySearch::GetFirstRow or IDirectorySearch::GetNextRow.

For Active Directory searches, the primary difference between synchronous and asynchronous is when the first row of the result is returned (that is, when the first GetFirstRow or GetNextRow call returns):

  • Synchronous. If paging is not enabled, the first row is returned when the server has constructed and returned the entire result set to the client.

    If paging is enabled, the first row is returned when the first page of the result set is returned.

  • Asynchronous. The first row is returned when the server has constructed the first row of the result set.

    If paging is enabled, the first row is returned when the first page of the result set is returned.

To specify the asynchronous option, include a ADS_SEARCHPREF_INFO structure with ADS_SEARCHPREF_ASYNCHRONOUS set to TRUE in the array passed in the IDirectorySearch::SetSearchPreferences call.

If the ADS_SEARCHPREF_ASYNCHRONOUS search preference is not set, the default is synchronous.


Paging

Paging specifies how many rows at a time the server returns to the client. A page can be defined by the number of rows or a time limit. The ADSI COM object takes care of retrieving each page of results based on the following settings. The caller simply calls IDirectorySearch::GetNextRow when the caller has reached the end of a page, and the ADSI COM object takes care of retrieving the next page.

ADS_SEARCHPREF_PAGESIZE
Specifies the number of rows to return in a page.
ADS_SEARCHPREF_PAGED_TIME_LIMIT
Specifies the maximum time (in seconds) that the server should spend collecting a page of results before returning the page to the client. If the limit is reached, the server stops searching and returns the rows retrieved for the page up to that point.

If neither of these search preferences is set, the default is no paging.

A search operation may result in a return of a large number of objects. If the server returns the result in one big chunk, it could downgrade the performance of the client and server as well as the network load. Paged search can be used to prevent this from happening. In a paged search, the client may elect to accept results in smaller chunks. The size of a chunk is known as the search page size.

Paged search offers benefits to both the client and the server. The client can be more responsive in presenting the results to end users. This is especially relevant to graphical user interface tools that can begin the window display process while the other thread concurrently receives the data.

On the server side, paged search makes the operation scalable. Suppose that one hundred clients issue search requests simultaneously and, on the average, each client gets back two hundred objects. If no page size is specified, the server must have sufficient memory to hold 20,000 objects in the worst case scenario. On the other hand, if each client specifies a page size to be, say, ten (10) objects, the memory requirement on the server is thus reduced by a factor of 20.

In addition, using a paged search, a client can abandon the operation in progress. In contrast, in a non-paged search, the client receives a result set in one big chunk. This may potentially put a heavy burden on the network.

On behalf of the client, ADSI handles the page size transparently. The client does not have to count the number of objects in progress. ADSI encapsulates the server interaction for the client. From the client's perspective, the search comes back with a complete result set.

It is recommended that you turn on paging.


Result Caching

The ADS_SEARCHPREF_CACHE_RESULTS preference caches the result set on the client. Result caching enables an application to retain a retrieved result set and go through the retrieved rows again. It also enables cursor support (the IDirectorySearch::GetNextRow and IDirectorySearch::GetPreviousRow methods can be used to go up and down the result set).

Recommendations:

  • Turn on caching if your application needs to go through the result set more than once without performing the search again on the server. If your application needs the result set and performing the search is expensive on the server (slow connection, large result set, or complex query), consider turning on caching.
  • Turn on caching if you need cursor support.
  • Turn off caching if your application needs to reduce memory requirements for caching a large result set at the client.

The default is for caching to be on.


Sorting the Search Results

The ADS_SEARCHPREF_SORT_ON preference tells the server to sort the result set. Use the ADS_SORTKEY structure to specify the attribute on which to sort.

It is recommended that you specify an indexed attribute. Otherwise, the server must retrieve the complete result set and sort it before sending any results to the client. This also applies to paged searches. Note that you will increase performance of a sorted search if the filter includes an indexed attribute and that attribute is specified as the sort key (in this case, Active Directory can satisfy the sort while processing the filter). For example, an efficient sort query for a set of users could have a filter that included (sn>smith) and a sort key of sn.

You should use sorting only if you really need it.

The default is for sorting to be off.


Referral Chasing

A referral is the mechanism that a directory server uses to direct a client to another server when it does not contain sufficient information about the object(s) requested by a query.

In a one-level or subtree search (see Search Scope), referrals are returned for known, immediately subordinate domain, schema or configuration containers only (that is, child domains that are direct descendants).

In a directory, not all information is available on a single server, rather, it is distributed over several different servers across the network. If the servers share the knowledge of the information that other servers can provide, they can provide referrals to a client when a requested query cannot be resolved on the originating server. For example, when a client asks Server A to query a user object (U), then A can suggest that the client continue the search on Server B if U does not reside on A, but is known to be on B. The client has the choice to pursue the referral or not. Search referrals free the client from possessing previous knowledge of the capability of each server. But the client must specify the type of referrals a server should make.

To enable referral chasing, use the ADS_SEARCHPREF_CHASE_REFERRALS preference with values from the ADS_CHASE_REFERRALS_ENUM enumeration. A client can choose any of the following four types of referral chasing:

  • Never. The server should not generate a referral to a client even though it knows that another server holds the requested information. This is the default setting.
  • External. The server should generate referrals if the request can be resolved on another server of a different directory tree. For example, a client queries OU=Sales, DC=ArcadiaBay, DC=COM on the msft01 server on the Microsoft.com domain. However, the object does not belong to msft01, but is known to be on the arc01 server on the ArcadiaBay.com domain. Thus, msft01 will refer arc01 to the client.
  • Subordinate. The server should generate referrals if the request can be resolved on a server whose name forms a contiguous path from the originating server. The search scope must be at the subtree level. For example, Server A contains objects in DC=Sales, DC=ArcadiaBay, DC=Com. Server B contains objects in DC=Seattle, DC=Sales, DC=ArcadiaBay, DC=Com. Notice that the name of Server B forms a contiguous path from Server A. When a client contacts Server A, requests a sub-tree search on DC=Sales, DC=ArcadiaBay, DC=Com, and specifies the referral to be the Subordinate type, the following event occurs:
    • Server A returns all objects that it knows within its scope.
    • Server A informs the client that objects in DC=Seattle, DC=Sales, DC=ArcadiaBay, DC=COM can be found on Server B.

    The client can choose to contact Server B. If so, the following event occurs:

    • Server B responds with the requested objects.
    • If Server B detects other servers on the contiguous naming path, the process continues.
  • Always. The server generates referrals if the search can be resolved based on either the external type or the subordinate type.

For more information, see Referrals.


Size Limit

To reduce the memory requirement or for other purposes, the client can focus on a small number of objects returned from the server and ignore the rest of the result set that are of no interest. To accomplish this, the client specifies the search size limit and other appropriate search criteria. For example, if the directory stores the SAT scores of a school district, you can query the top ten students with the highest SAT scores by specifying a size limit of 10 (ten) and a descending sort order.

The default for size limit is no limit.

Use the ADS_SEARCHPREF_SIZE_LIMIT preference to specify a size limit.


Server Time Limit

When you request a search on a server under a heavy workload, you may want to tell the server to restrict the search to a specified time limit. For example, you want to run an application to generate a weekly report on a server that is running near its capacity. To avoid using up all the CPU time and preventing other operations from running at all, you can specify the search time limit to a small value and then re-run the application later if it fails to generate the report.

Some servers might impose their own administrative time limit. In these cases, if you specify a search time limit value greater than the administrative time limit, the server will ignore your specification and use its internal time limit value instead.

The default time limit is no limit.

Use the ADS_SEARCHPREF_TIME_LIMIT preference to specify a server time limit.


Client Time-Out

A client can also impose a time limit that it is willing to wait for a server to return the result set. The value of the search time-out property specifies this client-side time limit. When the server fails to respond to a query within the specified time period, the client can abandon the search and try it again later.

The time-out property is useful when a client requests an asynchronous search. In an asynchronous search, the client makes a request and then proceeds with other tasks while waiting for the server to return the results. It is possible that the server can go offline without notifying the client. In this case, the client will have no way of knowing whether the server is still processing the query, or if it has ceased to be live. The time-out property gives the client some control of situations like this.

The default time-out is no limit.

Use the ADS_SEARCHPREF_TIMEOUT preference to specify a client time-out.


Returning Only Attribute Names

Sometimes you do a search to determine what type of information is available for a particular object. In this case, you are only interested in the attributes, not the values, of the object. To accomplish this, set the ADS_SEARCHPREF_ATTRIBTYPES_ONLY option. Specifying this option will return the attribute names without their values. However, the result set includes only those attributes that have values assigned. For example, consider an object with the following attributes:

name = James
sn = Smith
department = Empty
phone = (206) 555-0111

When the Search Attributes Only option is set, the result set includes:

name
sn
department
phone

The default is for both values and names to be returned.


Example Code for Searching for Attributes

The following function creates an array of ADS_SEARCHPREF_INFO structures and sets the specified search preferences. An application can use the returned array to call IDirectorySearch.

HRESULT SetSearchPreferences(
    DWORD dwScope,            //-1 means use default: subtree
    DWORD dwOverallTimeOut,   // 0 means use default: no time-out
    DWORD dwOverallSizeLimit, // 0 means use default: no size limit
    DWORD dwOverallTimeLimit, // 0 means use default: no time limit
    BOOL bCacheResult,        // True means use default.
    BOOL bIsAsynchronous,     // False means use default
    DWORD dwPageSize,         // 0 means use default
    DWORD dwPageTimeLimit,    // 0 means use default
    DWORD dwChaseReferral,    // 0 means use default
    LPOLESTR szSortKey,       // NULL means don't sort.
    BOOL bIsDescending,
    BOOL bReturnAttributeNamesOnly, //False means use default.
    ADS_SEARCHPREF_INFO **ppSearchPref, // Returns array of search preferences.
    DWORD *pdwSearchPrefCount
    )
{
HRESULT hr = S_OK;
DWORD dwCountPref = 0L;

//Determine size of preferences array.
DWORD dwTotal = 11L;

if(dwScope==-1)
  dwTotal--;
if(dwOverallTimeOut<=0)
  dwTotal--;
if(dwOverallSizeLimit<=0)
  dwTotal--;
if(dwOverallTimeLimit<=0)
  dwTotal--;
if(bCacheResult)
  dwTotal--;
if(!bIsAsynchronous)
  dwTotal--;
if(dwPageSize<=0)
  dwTotal--;
if(dwPageTimeLimit<=0)
  dwTotal--;
if(dwChaseReferral<=0)
  dwTotal--;
if(!bReturnAttributeNamesOnly)
  dwTotal--;
if (!szSortKey)
  dwTotal--;

ADS_SEARCHPREF_INFO *prefInfo = new ADS_SEARCHPREF_INFO[ dwTotal ];
ADS_SORTKEY SortKey;

    //////////////////
    // Search Scope
    //////////////////
    if(dwScope>=0)
  {
    prefInfo[dwCountPref].dwSearchPref =
                  ADS_SEARCHPREF_SEARCH_SCOPE;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
    prefInfo[dwCountPref].vValue.Integer = dwScope;
    dwCountPref++;
  }

    //////////////////
    // Time-out
    //////////////////
  if(dwOverallTimeOut>0)
  {
    prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_TIMEOUT;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
    prefInfo[dwCountPref].vValue.Integer = dwOverallTimeOut;
    dwCountPref++;
  }

    ///////////////
    // Size Limit
    ///////////////
  if(dwOverallSizeLimit>0)
  {
    prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_SIZE_LIMIT;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
    prefInfo[dwCountPref].vValue.Integer = dwOverallSizeLimit;
    dwCountPref++;
  }

    ///////////////
    // Time Limit
    ///////////////
  if(dwOverallTimeLimit>0)
  {
    prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_TIME_LIMIT;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
    prefInfo[dwCountPref].vValue.Integer = dwOverallTimeLimit;
    dwCountPref++;
  }

    /////////////////
    // Cache Result
    /////////////////

  if (!bCacheResult)
  {
    prefInfo[dwCountPref].dwSearchPref =
                ADS_SEARCHPREF_CACHE_RESULTS;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
    prefInfo[dwCountPref].vValue.Boolean = bCacheResult;
    dwCountPref++;
  }

    //////////////
    // Page Size
    //////////////
  if(dwPageSize>0)
  {
    prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;;
    prefInfo[dwCountPref].vValue.Integer = dwPageSize;
    dwCountPref++;
  }

    //////////////
    // Page Time Limit
    //////////////
  if(dwPageTimeLimit>0)
  {
    prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_PAGED_TIME_LIMIT;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;;
    prefInfo[dwCountPref].vValue.Integer = dwPageTimeLimit;
    dwCountPref++;
  }

    ///////////////////
    // Chase Referrals
    ///////////////////
  if(dwChaseReferral>0)
  {
    prefInfo[dwCountPref].dwSearchPref =
               ADS_SEARCHPREF_CHASE_REFERRALS;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
    prefInfo[dwCountPref].vValue.Integer = dwChaseReferral;
    dwCountPref++;
  }

    /////////////
    // Sort
    /////////////
    if (szSortKey)
    {
        prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_SORT_ON;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_PROV_SPECIFIC;
        SortKey.pszAttrType = (LPWSTR)LocalAlloc(
                        LPTR,
                        wcslen(szSortKey)*sizeof(WCHAR) +sizeof(WCHAR)
                        );
    wcscpy(SortKey.pszAttrType,szSortKey);
        SortKey.pszReserved = NULL;
        SortKey.fReverseorder = 0;
        prefInfo[dwCountPref].vValue.ProviderSpecific.dwLength = sizeof(ADS_SORTKEY);
        prefInfo[dwCountPref].vValue.ProviderSpecific.lpValue = (LPBYTE) &SortKey;
        dwCountPref++;
    }

    /////////////////
    // Asynchronous
    /////////////////
  if(bIsAsynchronous)
  {
    prefInfo[dwCountPref].dwSearchPref =
                ADS_SEARCHPREF_ASYNCHRONOUS;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
    prefInfo[dwCountPref].vValue.Integer = bIsAsynchronous;
    dwCountPref++;
  }

    ////////////////////////
    // Attribute Type Only
    ////////////////////////
  if(bReturnAttributeNamesOnly)
  {
    prefInfo[dwCountPref].dwSearchPref =
               ADS_SEARCHPREF_ATTRIBTYPES_ONLY;
    prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
    prefInfo[dwCountPref].vValue.Integer = bReturnAttributeNamesOnly;
    dwCountPref++;
  }

if (SUCCEEDED(hr))
{
  *pdwSearchPrefCount = dwCountPref;
  *ppSearchPref  = prefInfo;
}
else
{
  *pdwSearchPrefCount = 0L;
  *ppSearchPref  = NULL;
}

return hr;
}


Checking Search Preferences

The IDirectorySearch::SetSearchPreference method writes a status value to the dwStatus member of each ADS_SEARCHPREF_INFO structure specified in the method call to indicate whether the search preference was set. The ADS_STATUS enumeration defines the possible status values.


Example Code for Checking the Status of ADS_SEARCHPREF_INFO

The following function checks the status of an array of ADS_SEARCHPREF_INFO structures and displays the status in a message box:

HRESULT CheckPreferences(
     ADS_SEARCHPREF_INFO *pSearchPref, // Array of search preferences.
     DWORD dwSearchPrefCount
     )
{
  if ((!pSearchPref)||(dwSearchPrefCount<1))
    return E_INVALIDARG;
  HRESULT hr = S_OK;
  LPOLESTR szString = new OLECHAR[MAX_PATH*5];
  LPOLESTR szTemp = new OLECHAR[MAX_PATH];
  LPOLESTR szStatus = new OLECHAR[MAX_PATH];
  LPOLESTR szPref = new OLECHAR[MAX_PATH];
    wcscpy(szString,L"Check preferences:\r\n");
  for (DWORD i=0; i<dwSearchPrefCount; i++)
  {
         switch (pSearchPref[i].dwStatus)
     {
     case ADS_STATUS_S_OK:
            wcscpy(szStatus, L"ADS_STATUS_S_OK");
      break;
     case ADS_STATUS_INVALID_SEARCHPREF:
       wcscpy(szStatus, L"ADS_STATUS_INVALID_SEARCHPREF");
       break;
     case ADS_STATUS_INVALID_SEARCHPREFVALUE:
       wcscpy(szStatus, L"ADS_STATUS_INVALID_SEARCHPREFVALUE");
       break;
     default:
       swprintf(szStatus, L"Unknown status: %d",
                        pSearchPref[i].dwStatus);
       break;
     }
         switch (pSearchPref[i].dwSearchPref)
     {
     case ADS_SEARCHPREF_ASYNCHRONOUS:
             wcscpy(szPref, L"ADS_SEARCHPREF_ASYNCHRONOUS");
       break;
     case ADS_SEARCHPREF_SIZE_LIMIT:
       wcscpy(szPref, L"ADS_SEARCHPREF_SIZE_LIMIT");
       break;
     case ADS_SEARCHPREF_TIME_LIMIT:
       wcscpy(szPref, L"ADS_SEARCHPREF_TIME_LIMIT");
       break;
     case ADS_SEARCHPREF_ATTRIBTYPES_ONLY:
       wcscpy(szPref, L"ADS_SEARCHPREF_ATTRIBTYPES_ONLY");
       break;
     case ADS_SEARCHPREF_SEARCH_SCOPE:
       wcscpy(szPref, L"ADS_SEARCHPREF_SEARCH_SCOPE");
       break;
     case ADS_SEARCHPREF_TIMEOUT:
       wcscpy(szPref, L"ADS_SEARCHPREF_TIMEOUT");
       break;
     case ADS_SEARCHPREF_PAGESIZE:
       wcscpy(szPref, L"ADS_SEARCHPREF_PAGESIZE");
       break;
     case ADS_SEARCHPREF_PAGED_TIME_LIMIT:
       wcscpy(szPref, L"ADS_SEARCHPREF_PAGED_TIME_LIMIT");
       break;
     case ADS_SEARCHPREF_CHASE_REFERRALS:
       wcscpy(szPref, L"ADS_SEARCHPREF_CHASE_REFERRALS");
       break;
     case ADS_SEARCHPREF_SORT_ON:
       wcscpy(szPref, L"ADS_SEARCHPREF_SORT_ON");
       break;
     case ADS_SEARCHPREF_CACHE_RESULTS:
       wcscpy(szPref, L"ADS_SEARCHPREF_CACHE_RESULTS");
       break;
     default:
       swprintf(szPref, L"Unknown preference: %d",
                        pSearchPref[i].dwSearchPref);
       break;
     }
     swprintf(szTemp, L"%d. %s = %s\r\n",
                      i, szPref, szStatus );
     wcscat(szString,szTemp);
    }
  MessageBox(NULL,szString,L"Check Preference Status",MB_OK);

  return hr;
}


Effects of Security on Queries

Security is an implicit filter when performing searches, enumerating containers, or reading properties.

ADSI can return NO_SUCH_PROPERTY or NO_SUCH_OBJECT errors even when the object exists if you do not have access to read attributes on the object.

For example, a caller may be able to enumerate the child objects in a container because the caller has LIST_CONTENTS rights on the container. But the same caller may not be able to access the enumerated objects if the caller does not have read access to the child objects. In this case, a query for a child object may return NO_SUCH_OBJECT even though the caller successfully enumerated the object.

If the caller does not have sufficient rights, the following return codes may be returned:

E_ADS_INVALID_DOMAIN_OBJECT

E_ADS_PROPERTY_NOT_SUPPORTED

E_ADS_PROPERTY_NOT_FOUND


Processing Query Results

After the first call to IDirectorySearch::GetFirstRow or IDirectorySearch::GetNextRow, either S_OK, S_ADS_NOMORE_ROWS, or an error result is returned.

If the return value is S_ADS_NOMORE_ROWS, no object matching the filter was found. If an error result is returned, the query failed. In both cases, you don't need to process the rows in the result because nothing was returned.

If the return is S_OK, a row has been retrieved. You can iterate through the columns by name using IDirectorySearch::GetColumn. The name is the lDAPDisplayName of the attribute in the column. The set of all columns was defined by the pAttributeNames parameter of the IDirectorySearch::ExecuteSearch method. If NULL was specified, the set of all columns is the union of all properties found for all the objects returned. To read the entire set of columns returned for an object, use the IDirectorySearch::GetNextColumnName to iterate each column, and use the column name returned to call IDirectorySearch::GetColumn.

The IDirectorySearch::GetColumn method returns an ADS_SEARCH_COLUMN structure that contains the attribute name, the type of the attribute, count of values, and a pointer to an array of ADSVALUE structures that contain the values. You can loop through the ADSVALUE structures to read the values for the property returned by the column. You must read the appropriate member of the ADSVALUE structure based on the ADSTYPE specified by the dwADsType member of the ADS_SEARCH_COLUMN structure (or the dwType member of the ADSVALUE structure). For example, if dwADsType was ADSTYPE_INTEGER, you would read the Integer member of each ADSVALUE structure.

For sample code, see Example Code for Searching for Users.


Creating Efficient Queries

There are several concepts you should consider when executing a query. The following list identifies the most important ones.

  • Make sure the query filter contains at least one indexed attribute. See Indexed Attributes.
  • Search on objectCategory instead of objectClass, since objectClass is not an indexed property. The statement (objectClass=foo) refers to directory objects in which foo represents any class in the object's class hierarchy, whereas (objectCategory=foo), refers to those directory objects in which foo identifies a specific class in the object's class hierarchy. The objectClass property can take multiple values, whereas objectCategory takes a single value and is, thus, better suited for type matching of objects in a directory search. ADSI uses this as the default matching criterion. Searches using one objectClass are not scalable to large databases. ADSI supports (objectCategory=SomeDN) and (objectCategory=Ldap_Display_Name_of_Class), for example, (objectCategory=user). The exception to this is that the LDAP search filter (objectClass=*) does not specify a search on object class, but merely tests for the presence of the objects.
  • Avoid searching for text in the middle and on the end of a string. For example, "cn=*hille*" or "cn=*larouse". Using more specific matching criteria tends to boost search performance. This is because Active Directory™ evaluates all predicates, identifies the indices, and then picks one index that it considers most likely to yield the smallest set of returned values.
  • Chasing referrals is expensive. Try to take advantage of the global catalog if you are considering subtree searches. See Referral Chasing.
  • Assume a subtree search will return a large result set. Use paging when performing subtree searches. The server will then be able to stream a large result set in chunks reducing the server side memory resources. This effectively flattens out network usage and reduces the need for sending extremely large chunks of data over a network. See Paging.
  • Restrict queries to retrieve only what is necessary.
  • One search of an object that reads two attributes is cheaper than two searches of the same object, each returning one attribute.
  • Bind to an object once and hang onto the binding handle for the rest of your session. Do not bind and unbind for each call. If you are using ADO or OLE DB, do not create many connection objects.
  • Read the rootDSE once and remember its contents for the rest of your session. See Serverless Binding and RootDSE in the Binding chapter.
  • Persist references to objects as GUIDs, not distinguished names, in order to be rename and delete safe. For more information, see Using objectGUID to Bind to an Object.


Referrals

Active Directory maintains referral information in crossRef objects stored in the partitions container (crossRefContainer) in the configuration container. Earlier in this chapter (the Where to Search section), referrals were discussed in the context of a domain within a domain tree and the generation of referrals to subordinate domains on a subtree search.

Active Directory automatically creates and maintains crossref objects for all domains in the forest. In addition, there are crossRef objects for the configuration and schema containers. These crossRef objects are used to generate referrals in queries that request information about objects that are in the forest, but not contained on the directory server handling the request. These are called internal cross references, because they refer to domains, schema, and configuration containers within the forest itself. In the case of a subtree search, the directory server returns referrals to the subordinate domains that are direct descendants of the directory server's domain. It is up to the client to resolve the referrals by binding to the path specified by the referral and submitting a query. If referral chasing is turned on, the WLDAP32.DLL library takes care of chasing the referral. If referral chasing is turned off, the calling application receives the referral information and then it can decide whether to chase the referral.

In addition to the dnsRoot (DNS name of the domain) and nCName (distinguished name for the domain) properties, the crossRef object also contains the nETBIOSName (NetBIOS name of the domain) and trustParent (distinguished name for the crossRef object representing the domain's direct parent domain) properties.

Active Directory can also have external cross references that refer to objects outside of the forest. External cross references must be added explicitly by an administrator. Note that the target server of the external cross reference must have a DNS root, that is, it must adhere to RFC 2247.

External cross references are used for the following purposes:

  • To refer to a name that is totally disjointed from any name in the forest. The "name" can refer to another LDAP server on your network or on the Internet. For example, LDAP client applications used by your company may submit operations to your directory servers for arcadiabay.com. You create a crossRef object for arcadiabay.com. Now, instead of returning an object not found error, your directory servers can return a referral to arcadiabay.com and the clients can chase the referral.
  • To refer to an immediate child of an object in the forest.

    For example, suppose you have an object with the following distinguished name:

    CN=SomeObject,OU=SomeOU,DC=arcadiabay,DC=Com

    You can add an external cross reference for an object with the name ChildOfSomeObject:

    CN=ChildOfSomeObject,CN=SomeObject,OU=SomeOU,DC=arcadiabay,DC=Com

    A subtree search that contains SomeObject will also return a referral to ChildOfSomeObject. Note that there really exists an LDAP server at the address specified by the referral (one of the properties on the crossRef object) and that this LDAP server serves the namespace identified by ChildOfSomeObject.

If referral chasing is turned on, the WLDAP32.DLL library will chase the referral by binding to the server specified by the dNSRoot property of the crossRef object and continue the search on the distinguished name specified by the nCName property or a specific object within the naming context specified by nCName. The credentials used to read the crossRef object are used to bind to the server specified by dNSRoot. Note that the search results from the referral will be returned as part of the results for the original search. This means the client application will see all the rows for both the original search and any referred searches as a single result set.

If referral chasing is turned off, the calling application receives the referral information and then it can decide whether to chase the referral. To chase the referral, the client application must submit another search request to the server, distinguished name, and query filter specified by the referral. Referrals resolved in this way will return separate result sets from the original search request.

Since crossRef objects are stored in the configuration container, every domain controller (DC) has a copy of all crossRef objects. Therefore, every DC contains information about every domain in the forest (as well as their superior/subordinate relationships). This gives every DC the ability to generate referrals to any domain in the forest and referrals for unexplored subordinate domain, schema, or configuration containers on a subtree search.


Example Code for Binding to a Partitions Container

The following examples bind to the partitions container and enumerate all crossRef objects.

[C++]
// This application enumerates crossrefs in the partitions container.
//
#include <objbase.h>
#include <wchar.h>
#include <activeds.h>

//Make sure you define UNICODE
//Need to define version 5 for Windows 2000
#define _WIN32_WINNT 0x0500

#include <sddl.h>

HRESULT FindCrossRefs(IDirectorySearch *pConfigNC,  //IDirectorySearch pointer to Partitions container.
                      LPOLESTR szFilter, //Filter for finding specific crossrefs.
                                         //NULL returns all attributeSchema objects.
                      LPOLESTR *pszPropertiesToReturn, //Properties to return for crossRef objects found
                                                       //NULL returns all set properties.
                      BOOL bIsVerbose //TRUE means all properties for the found objects are displayed.
                                      //FALSE means only the RDN
                     );

void wmain( int argc, wchar_t *argv[ ])
{
  //Handle the command line arguments.
  LPOLESTR pszBuffer = new OLECHAR[MAX_PATH*2];
  wcscpy(pszBuffer, L"");

  BOOL bReturnVerbose = FALSE;
  wprintf(L"\nFinding all crossRef objects in the Partitions container\n");

  //Intialize COM
  CoInitialize(NULL);
  HRESULT hr = S_OK;

  //Get rootDSE and the config container's DN.
  IADs *pObject = NULL;
  IDirectorySearch *pConfigNC = NULL;
  LPOLESTR szPath = new OLECHAR[MAX_PATH];
  VARIANT var;
  hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)&pObject);

  if (FAILED(hr))
  {
    wprintf(L"Could not execute query. Could not bind to LDAP://rootDSE. HR: %x\n",hr);

    if (pObject)
      pObject->Release();

    return;
  }

  if (SUCCEEDED(hr))
  {
    hr = pObject->Get(L"configurationNamingContext",&var);

    if (SUCCEEDED(hr))
    {
      //Build path to the partitions container.
          wcscpy(szPath,L"LDAP://cn=Partitions,");
          wcscat(szPath,var.bstrVal);
          hr = ADsOpenObject(szPath,
                             NULL,
                             NULL,
                             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                             IID_IDirectorySearch,
                             (void**)&pConfigNC);

      if (SUCCEEDED(hr))
      {
        hr = FindCrossRefs(pConfigNC, //IDirectorySearch pointer to Partitions container.
                           NULL, //Find all
                           NULL, //Return all properties
                           TRUE //Display all properties
                          );

        if (SUCCEEDED(hr))
        {
          if (S_FALSE==hr)
            wprintf(L"No crossRef object could be found.\n");
        }
        else if (0x8007203e==hr)
          wprintf(L"Could not execute query. An invalid filter was specified.\n");
        else
          wprintf(L"Query failed to run. HRESULT: %x\n",hr);
      }
      else
      {
         wprintf(L"Could not execute query. Could not bind to the schema container.\n");
      }

      if (pConfigNC)
        pConfigNC->Release();

    }

    VariantClear(&var);

  }

  if (pObject)
    pObject->Release();

  // Uninitialize COM
  CoUninitialize();
  return;
}

HRESULT FindCrossRefs(IDirectorySearch *pConfigNC,  //IDirectorySearch pointer to Partitions container.
                      LPOLESTR szFilter, //Filter for finding specific crossrefs.
                                         //NULL returns all attributeSchema objects.
                      LPOLESTR *pszPropertiesToReturn, //Properties to return for crossRef objects found
                                                       //NULL returns all set properties.
                      BOOL bIsVerbose //TRUE means all properties for the found objects are displayed.
                                      //FALSE means only the RDN
                     )
{
  if (!pConfigNC)
    return E_POINTER;

  //Create search filter
  LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];
  LPOLESTR szCategory = NULL;

  wsprintf(pszSearchFilter, L"(&(objectCategory=crossRef)%s)",szFilter);

  //Attributes are one-level deep in the schema container so only need to search one level.
  ADS_SEARCHPREF_INFO SearchPrefs;
  SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
  SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
  SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
  DWORD dwNumPrefs = 1;

  // COL for iterations
  LPOLESTR pszColumn = NULL;
  ADS_SEARCH_COLUMN col;
  HRESULT hr;

  // Interface Pointers
  IADs    *pObj = NULL;
  IADs  * pIADs = NULL;

  // Handle used for searching
  ADS_SEARCH_HANDLE hSearch = NULL;

  // Set the search preference
  hr = pConfigNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);

  if (FAILED(hr))
    return hr;

  LPOLESTR pszBool = NULL;
  DWORD dwBool;
  PSID pObjectSID = NULL;
  LPOLESTR szSID = NULL;
  LPOLESTR szDSGUID = new WCHAR [39];
  LPGUID pObjectGUID = NULL;
  FILETIME filetime;
  SYSTEMTIME systemtime;
  DATE date;
  VARIANT varDate;
  LARGE_INTEGER liValue;
  LPOLESTR *pszPropertyList = NULL;
  LPOLESTR pszNonVerboseList[] = {L"lDAPDisplayName",L"cn"};

  LPOLESTR szCNValue = new OLECHAR[MAX_PATH];
  LPOLESTR szLDAPDispleyNameValue = new OLECHAR[MAX_PATH];

  int iCount = 0;
  DWORD x = 0L;

  if (!bIsVerbose)
  {
    //Return non-verbose list properties only
    hr = pConfigNC->ExecuteSearch(pszSearchFilter,
                                  pszNonVerboseList,
                                  sizeof(pszNonVerboseList)/sizeof(LPOLESTR),
                                  &hSearch
                                 );
  }
  else
  {
    if (!pszPropertiesToReturn)
    {
      //Return all properties.
      hr = pConfigNC->ExecuteSearch(pszSearchFilter,
                                    NULL,
                                    0L,
                                    &hSearch
                                   );
    }
    else
    {
      //specified subset.
      pszPropertyList = pszPropertiesToReturn;
      //Return specified properties
      hr = pConfigNC->ExecuteSearch(pszSearchFilter,
                                    pszPropertyList,
                                    sizeof(pszPropertyList)/sizeof(LPOLESTR),
                                    &hSearch
                                   );
    }
  }

  if ( SUCCEEDED(hr) )
  {
    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
    hr = pConfigNC->GetFirstRow( hSearch);

    if (SUCCEEDED(hr))
    {
      while( hr != S_ADS_NOMORE_ROWS )
      {
        //Keep track of count.
        iCount++;

        if (bIsVerbose)
          wprintf(L"----------------------------------\n");

        // loop through the array of passed column names,
        // print the data for each column
        while( pConfigNC->GetNextColumnName( hSearch, &pszColumn ) != S_ADS_NOMORE_COLUMNS )
        {
          hr = pConfigNC->GetColumn( hSearch, pszColumn, &col );

          if ( SUCCEEDED(hr) )
          {
            // Print the data for the column and free the column
            if(bIsVerbose)
            {
              // Get the data for this column
              wprintf(L"%s\n",col.pszAttrName);

              switch (col.dwADsType)
              {
                case ADSTYPE_DN_STRING:
                  for (x = 0; x< col.dwNumValues; x++)
                  {
                    wprintf(L"  %s\r\n",col.pADsValues[x].DNString);
                  }
                  break;

                  case ADSTYPE_CASE_EXACT_STRING:
                  case ADSTYPE_CASE_IGNORE_STRING:
                  case ADSTYPE_PRINTABLE_STRING:
                  case ADSTYPE_NUMERIC_STRING:
                  case ADSTYPE_TYPEDNAME:
                  case ADSTYPE_FAXNUMBER:
                  case ADSTYPE_PATH:
                  case ADSTYPE_OBJECT_CLASS:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      wprintf(L"  %s\r\n",col.pADsValues[x].CaseIgnoreString);
                    }
                    break;

                  case ADSTYPE_BOOLEAN:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      dwBool = col.pADsValues[x].Boolean;
                      pszBool = dwBool ? L"TRUE" : L"FALSE";
                      wprintf(L"  %s\r\n",pszBool);
                    }
                    break;

                  case ADSTYPE_INTEGER:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      wprintf(L"  %d\r\n",col.pADsValues[x].Integer);
                    }
                    break;

                  case ADSTYPE_OCTET_STRING:
                    if ( _wcsicmp(col.pszAttrName,L"objectSID") == 0 )
                    {
                      for (x = 0; x< col.dwNumValues; x++)
                      {
                        pObjectSID = (PSID)(col.pADsValues[x].OctetString.lpValue);

                        //Convert SID to string.
                        ConvertSidToStringSid(pObjectSID, &szSID);
                        wprintf(L"  %s\r\n",szSID);

                        LocalFree(szSID);
                      }
                    }
                    else if ( (_wcsicmp(col.pszAttrName,L"objectGUID") == 0)
                      || (_wcsicmp(col.pszAttrName,L"schemaIDGUID") == 0)
                      || (_wcsicmp(col.pszAttrName,L"attributeSecurityGUID") == 0) )
                    {
                      for (x = 0; x< col.dwNumValues; x++)
                      {
                        //Cast to LPGUID
                        pObjectGUID = (LPGUID)(col.pADsValues[x].OctetString.lpValue);

                        //Convert GUID to string.
                        ::StringFromGUID2(*pObjectGUID, szDSGUID, 39);

                        //Print the GUID
                        wprintf(L"  %s\r\n",szDSGUID);
                      }
                    }
                    else if ( _wcsicmp(col.pszAttrName,L"oMObjectClass") == 0 )
                    {
                      //TODO:
                      wprintf(L"  TODO:No conversion for this.");
                    }
                    else
                      wprintf(L"  Value of type Octet String. No Conversion.");

                    break;

                  case ADSTYPE_UTC_TIME:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      systemtime = col.pADsValues[x].UTCTime;

                      if (SystemTimeToVariantTime(&systemtime, &date) != 0)
                      {
                        //Pack in variant.vt
                        varDate.vt = VT_DATE;
                        varDate.date = date;
                        VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);
                        wprintf(L"  %s\r\n",varDate.bstrVal);
                        VariantClear(&varDate);
                      }
                      else
                        wprintf(L"  Could not convert UTC-Time.\n",pszColumn);
                    }

                    break;

                  case ADSTYPE_LARGE_INTEGER:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      liValue = col.pADsValues[x].LargeInteger;
                      filetime.dwLowDateTime = liValue.LowPart;
                      filetime.dwHighDateTime = liValue.HighPart;

                      if((filetime.dwHighDateTime==0) && (filetime.dwLowDateTime==0))
                      {
                        wprintf(L"  No value set.\n");
                      }
                      else
                      {
                        //Check for properties of type LargeInteger that represent time
                        //if TRUE, then convert to variant time.
                        if ((0==wcscmp(L"accountExpires", col.pszAttrName))|
                           (0==wcscmp(L"badPasswordTime", col.pszAttrName))||
                           (0==wcscmp(L"lastLogon", col.pszAttrName))||
                           (0==wcscmp(L"lastLogoff", col.pszAttrName))||
                           (0==wcscmp(L"lockoutTime", col.pszAttrName))||
                           (0==wcscmp(L"pwdLastSet", col.pszAttrName))
                          )
                        {
                          //Handle special case for Never Expires where low part is -1
                          if (filetime.dwLowDateTime==-1)
                          {
                            wprintf(L"  Never Expires.\n");
                          }
                          else
                          {
                            if (FileTimeToLocalFileTime(&filetime, &filetime) != 0)
                            {
                              if (FileTimeToSystemTime(&filetime,
                                                       &systemtime) != 0)
                              {
                                if (SystemTimeToVariantTime(&systemtime,
                                                            &date) != 0)
                                {
                                  //Pack in variant.vt
                                  varDate.vt = VT_DATE;
                                  varDate.date = date;
                                  VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);
                                  wprintf(L"  %s\r\n",varDate.bstrVal);
                                  VariantClear(&varDate);
                                }
                                else
                                {
                                  wprintf(L"  FileTimeToVariantTime failed\n");
                                }

                              }
                              else
                              {
                                wprintf(L"  FileTimeToSystemTime failed\n");
                              }

                            }
                            else
                            {
                              wprintf(L"  FileTimeToLocalFileTime failed\n");
                            }
                          }
                        }
                        else
                        {
                          //Print the LargeInteger.
                          wprintf(L"  high: %d low: %d\r\n",filetime.dwHighDateTime, filetime.dwLowDateTime);
                        }
                      }
                    }
                        break;

                  case ADSTYPE_NT_SECURITY_DESCRIPTOR:
                    for (x = 0; x< col.dwNumValues; x++)
                    {
                      wprintf(L"  Security descriptor.\n");
                    }
                    break;

                  default:
                    wprintf(L"Unknown type %d.\n",col.dwADsType);
                }
            }
            else
            {
              //Verbose handles only the two single-valued attributes: cn and ldapdisplayname
              //so this is a special case.
              if (0==wcscmp(L"cn", pszColumn))
              {
                wcscpy(szCNValue,col.pADsValues->CaseIgnoreString);
              }

              if (0==wcscmp(L"lDAPDisplayName", pszColumn))
              {
                wcscpy(szLDAPDispleyNameValue,col.pADsValues->CaseIgnoreString);
              }
            }

            pConfigNC->FreeColumn( &col );
          }

        FreeADsMem( pszColumn );
      }

      if (!bIsVerbose)
        wprintf(L"%s (%s)\n",szLDAPDispleyNameValue,szCNValue);
        //Get the next row
        hr = pConfigNC->GetNextRow( hSearch);
    }

  }

  // Close the search handle to clean up
  pConfigNC->CloseSearchHandle(hSearch);

  }

  if (SUCCEEDED(hr) && 0==iCount)
    hr = S_FALSE;

  return hr;

}
[VBScript]

'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
On Error Resume Next

msgbox "This script enumerates crossRef objects in the partitions container."

sPrefix = "LDAP://"

'Get distinguished name for config container and build ADsPath to partitions container.
  Set root= GetObject(sPrefix & "rootDSE")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject method for rootDSE"
  End If
  sConfigDN = root.Get("configurationNamingContext")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Get method"
  End If
  sContainerDN = "cn=Partitions," & sConfigDN

'''''''''''''''''''''''''''''''''''''''
'Bind to the container
'''''''''''''''''''''''''''''''''''''''
Set cont= GetObject(sPrefix & sContainerDN)
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on GetObject method for partitions container"
End If
'''''''''''''''''''''''''''''''''''''''
'Enumerate the container.
''''''''''''''''''''''''''''''''''''''
For Each obj In cont
 strText = strText & "Name: " & obj.Get("name") & vbCrLf
 values = obj.GetEx("objectClass")
 For Each value In values
  sValue = value
 Next
 strText = strText & "  objectClass: " & sValue & vbCrLf
 strText = strText & "  DnsRoot: " & obj.Get("dnsRoot") & vbCrLf
 strText = strText & "  NCName: " & obj.Get("NCName") & vbCrLf
 sTrustParent = obj.Get("trustParent")
 If (Err.Number = 0) Then
   strText = strText & "  TrustParent: " & sTrustParent & vbCrLf
 Else
   Err.Clear
 End If
 sNetBIOSName = obj.Get("nETBIOSName")
 If (Err.Number = 0) Then
   strText = strText & "  NETBIOSName: " & sNetBIOSName & vbCrLf
 Else
   Err.Clear
 End If

Next
 show_items strText, "Display crossRef"
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Create CrossRef"
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


When Referrals Are Generated

A referral is the way that a directory server communicates that it does not contain the information required to complete a request (such as a query), but has a reference to a server that may contain the required information. Note that referrals are not just generated by query requests.

The following operations can result in one or more referrals:

  • Binding to a server that does not contain the object specified by the requested distinguished name but has information about a server or domain that may contain that object. For ADSI, this can occur if the application calls ADsGetObject or ADsOpenObject to bind to an object that exists in another domain in the forest (internal referral) or a naming context that is completely separate from the forest (external referral). For the LDAP API, this can occur when performing add, modify, delete, or search operations that specify an object that exists in another domain in the forest (internal referral) or a naming context that is completely separate from the forest (external referral).

    What about other forests? A Windows 2000 domain controller will automatically generate an external referral based on the domain controller components of the distinguished name. If name resolution fails to find an object locally and there are no crossRef objects for that portion of namespace, the domain controller will attempt the following: 1) Check if the distinguished name specified has DC= naming attributes for its upper components 2) If it does, it constructs an external referral based on the upper components of the distinguished name. For example, if your search was based at "cn=a,cn=b,dc=c,dc=d,dc=e", the domain controller will construct a referral to the LDAP server at DNS address "c.d.e".

    This means that all Windows 2000 domain controllers (which support only DC= naming for the upper components) automatically have knowledge of each other, and no external crossrefs are ever required for a client to bind from one forest to another. If other non-Windows 2000 directory servers (such as a Netscape server) is using DC= naming and has an appropriate SRV RR registered in DNS, it will get the advantage of the automatic referrals as well. If not, an external crossRef object must be added manually.

  • Performing a subtree search on a domain that contains subordinate domains in the forest or subordinate external domains, schema, or configuration containers.


Creating an External Referral

If you create an external crossRef and a domain controller uses it to generate a referral (see the previous section for situations where this can occur), the crossRef provides the two key pieces of information in the following properties:

dnsRoot
Specifies the server or domain that can serve information from the naming context specified in nCName.
nCName
Specifies the distinguished name for the domain, schema, or configuration container rooted at the server or domain specified by dnsRoot.

For example, if the server with DNS address of serv1.northwest.arcadiabay.com serves the naming context rooted at CN=MyContainer,OU=MyDOM,O=mycompany, you would set the dnsRoot to that server's DNS address and the nCName to the distinguished name of the domain, schema, or configuration container.


Example Code for Creating an External crossRef Object

The following VBScript examples create an external crossRef object.

'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
On Error Resume Next
Set oArgs = WScript.Arguments

If oArgs.Count = 3 Then
  sCrossRefCN = oArgs.item(0)
  sCrossRefDNSRoot = oArgs.item(1)
  sCrossRefNCName = oArgs.item(2)
Else
  sCrossRefCN = InputBox("This script creates a cross reference object." & vbCrLf & vbCrLf &"Specify the name (cn) of the new crossRef object:")
  sCrossRefDNSRoot = InputBox("Specify the DNS root of the server or domain containing the external naming context:")
  sCrossRefNCName = InputBox("Specify the distinguished name (DN) of the external naming context:")
End If

If sCrossRefCN = "" Then
  WScript.Echo "No CN was specified. You must specify a CN."
  WScript.Quit(1)
End If

If sCrossRefNCName = "" Then
  WScript.Echo "No DN was specified. You must specify a DN."
  WScript.Quit(1)
End If

If sCrossRefDNSRoot = "" Then
  WScript.Echo "No DNS root was specified. You must specify a DNS root."
  WScript.Quit(1)
End If

sPrefix = "LDAP://"

'Get distinguished name for config container and build ADsPath to partitions container.
  Set root= GetObject(sPrefix & "rootDSE")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject method"
  End If
  sConfigDN = root.Get("configurationNamingContext")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Get method"
  End If
  sContainerDN = "cn=Partitions," & sConfigDN

'''''''''''''''''''''''''''''''''''''''
'Bind to the container
'''''''''''''''''''''''''''''''''''''''
Set cont= GetObject(sPrefix & sContainerDN)
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on GetObject method for partitions container"
End If
'''''''''''''''''''''''''''''''''''''''
'Add the user
'''''''''''''''''''''''''''''''''''''''
Set newCrossRef  = cont.Create("crossRef", "cn=" & sCrossRefCN)
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Create method"
End If
newCrossRef.put "dnsRoot", sCrossRefDNSRoot
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Put method call for dnsRoot"
End If
newCrossRef.put "nCName", sCrossRefNCName
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Put method call for nCName"
End If
newCrossRef.SetInfo
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on SetInfo method"
End If
strText = "The crossRef " & sCrossRefCM & " was successfully added."
strText = strText & vbCrLf & "The crossRef has the following properties:"
'Refresh the property cache
newCrossRef.GetInfo
count = newCrossRef.PropertyCount
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on PropertyCount method"
End If
strText = strText & "Number of properties: " & count

'Set v = user.Next()
'cprop = 1
'If (Err.Number <> 0) Then
'  BailOnFailure Err.Number, "on Next method"
'End If

'While (Not (IsNull(v)) And Err.Number = 0)
'  cprop = cprop+1
'  Set v = user.Next()
'If (Err.Number <> 0) Then
'  BailOnFailure Err.Number, "on Next method"
'End If
'Wend
For cprop=1 to count
  Set v = newCrossRef.Next()
  If IsNull(v) Then
    Exit For
  End If
  strText = strText & vbCrLf & cprop & ") " & v.Name & " "
  cattr = 0
  attr = v.Values
  For Each attrval In attr
    cattr = cattr+1
    if (v.adstype=3) then
      strText = strText & vbCrLf & "      " & attrval.caseignorestring
    end if
    if (v.adstype=1) then
      strText = strText & vbCrLf & "      " & attrval.DNstring
    end if
    if (v.adstype=7) then
      strText = strText & vbCrLf & "      " & attrval.Integer
    end if
    if (v.adstype=6) then
      strText = strText & vbCrLf & "      " & attrval.Boolean
    end if
    if (v.adstype=9) then
      strText = strText & vbCrLf & "      " & attrval.UTCTime
    end if
    if (v.adstype=10) then
      strText = strText & vbCrLf & "      " &  "Type: LargeInteger"
    end if
    if (v.adstype=8) then
      strText = strText & vbCrLf & "      " &  "Type: OctetString"
    end if
    if (v.adstype=25) then
      strText = strText & vbCrLf & "      " &  "Type: NTSecurityDescriptor"
    end if
  Next

Next
show_items strText, "Create crossRef"

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Create CrossRef"
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Searching with IDirectorySearch

The IDirectorySearch interface provides a very high level and low overhead interface for querying data of a directory or a global catalog. The IDirectorySearch COM interface is a vtable only interface and thus, is not available to development environments that use Automation (that is, Visual Basic, VBScript, and so on).

To perform a search

  1. Bind to an object in the directory
  2. Call QueryInterface to get the IDirectorySearch pointer
  3. Execute the search using the IDirectorySearch pointer. Call the IDirectorySearch::ExecuteSearch method, passing a search filter, the requested attribute names, and other parameters.

Execution of a query is provider specific. In some providers, the actual query execution does not take place until IDirectorySearch::GetFirstRow or IDirectorySearch::GetNextRow is called. The IDirectorySearch interface works with search filters directly. Neither the SQL dialect nor the LDAP dialect are required.

The IDirectorySearch interface provides methods to enumerate the result set, row by row. The IDirectorySearch::GetFirstRow method retrieves the first row and IDirectorySearch::GetNextRow moves you to the next row from the current row. When you have reached the last row, calling these methods returns the S_ADS_NOMORE_ROWS error code. On the other hand, IDirectorySearch::GetPreviousRow moves you back one row at a time. And an S_ADS_NOMORE_ROWS return value indicates that you have reached the first row of the result set. These methods operate on the result set resident in memory on the client. Thus, when paged and asynchronous searches are performed, and the ADS_SEARCHPREF_CACHE_RESULTS option is turned off, backward scrolling could have unexpected consequences.

Once you have located the appropriate row, you can call IDirectorySearch::GetColumn to obtain data items, column by column. For each call, you pass in the name of the column of interest. The data item returned is a pointer to an ADS_SEARCH_COLUMN structure. GetColumn allocates this structure for you, but you must free it, using FreeColumn. You must also call CloseSearchHandle to finish the search operation.

IDirectorySearch methods | Description
SetSearchPreference | Sets options for conducting a search.
ExecuteSearch | Executes an individual search.
AbandonSearch | Abandons a search already under way.
GetFirstRow | Gets the first row of the search result.
GetNextRow | Gets the next row of the search result.
GetPreviousRow | Gets the previous row of the search result.
GetNextColumnName | Gets the name of the next column of the search result.
GetColumn | Gets the item in a specified column from the current row of the search result.
FreeColumn | Frees the ADS_SEARCH_COLUMN structure created by GetColumn.
CloseSearchHandle | Releases the search result from memory.

To perform a directory search

  1. Bind to an LDAP provider (may be a domain controller or a global catalog provider).
  2. Retrieve the IDirectorySearch COM Interface. This may be done with a call to QueryInterface or may have been done in Step 1 during the initial binding.
  3. Optionally, call SetSearchPreference to select options for handling the results of your search.
  4. Call ExecuteSearch. Depending on the options set in SetSearchPreference, this may or may not begin execution of the query.
  5. Call GetNextRow to move the row index (internal to IDirectorySearch) to the first row.
  6. Read the data from the row using GetColumn, then call FreeColumn to release the memory allocated by GetColumn.
  7. Repeat Step 5 until finished retrieving data from the search result, or until GetNextRow returns S_ADS_NOMORE_ROWS.
  8. When done, call AbandonSearch and CloseSearchHandle.

As an example of the scenario above, we start out binding to ADSI by calling ADsOpenObject:

HRESULT hr; // COM result variable
ADS_SEARCH_COLUMN col;
// COL for iterations

// Interface Pointers
IDirectorySearch   *pDSSearch  =NULL;
IADs        *pObj     = NULL;
IADs        *pIADs    = NULL;

// Initialize COM
CoInitialize(0);

// Open a connection with the server.
hr = ADsGetObject(L"LDAP://myserver",
                  IID_IDirectorySearch,
                  (void **)&pDSSearch);

This gets us a pointer to the IDirectorySearch interface.

Now that we have a COM interface to an IDirectoryInterface instance, we may wish to call IDirectorySearch::SetSearchPreference. We will skip this for the moment as IDirectorySearch::SetSearchPreference is covered in detail in the next section.

Next we need to prepare to call the IDirectorySearch::ExecuteSearch function by building an array of attribute names. The attribute names are defined within the schema of Active Directory™. Refer to the ADSI section of the Platform SDK documentation to see the schema definition. The attribute names listed are returned (if supported by the object) as the result set of the search.

LPWSTR pszAttr[] = { L"description", L"Name", L"distinguishedname" };
ADS_SEARCH_HANDLE hSearch;
DWORD dwCount = 0;
DWORD dwAttrNameSize = sizeof(pszAttr)/sizeof(LPWSTR);

Now we call ExecuteSearch. The search does not actually take place, however, until we call GetNextRow.

// Search for all objects with the 'cn' property beginning with c
hr = pDSSearch->ExecuteSearch(L"(cn=c*)",pszAttr ,dwAttrNameSize,&hSearch );

Now we call GetNextRow to iterate through the rows in the result. Each row is then queried for the "description" attribute. If the attribute is found, it is displayed.

LPWSTR pszColumn;
    while( pDSSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
    {
        // Get the property.
        hr = pDSSearch->GetColumn( hSearch, L"description", &col );

        // If this object SUPPORTS this attribue, display it.
        if ( SUCCEEDED(hr) ) {
            wprintf(L"The description property:%s\r\n",
                    col.pADsValues->CaseIgnoreString);
            pDSSearch->FreeColumn( &col );
        }
        else
            puts("description property NOT available");
        puts("------------------------------------------------");
        dwCount++;
    }
    pDSSearch->CloseSearchHandle(hSearch);

We can call AbandonSearch to end the search at any time.

Note If a page size is NOT set, GetNextRow will block until the entire result set is returned to the client. If page size IS set, then GetNextRow will block until the first page (page size = number of rows in a page) is returned. If page size is set and the query is to be sorted and you are NOT searching on at least one indexed attribute, then the page size value is ignored, and the server calculates the entire result set prior to returning the data. This has the effect of GetNextRow blocking until the query completes.


Setting Search Preferences

Calling IDirectorySearch::SetSearchPreference changes the way in which the resulting search data is obtained and presented through the IDirectorySearch interface.

Multiple preferences may be set by passing an array as the first parameter and the array size as the second parameter.

This example sets the page size to 100 rows and the scope to be of type ADS_SCOPE_SUBTREE. The page size setting causes the server to immediately return data to the client, after 100 rows have been calculated. The ADS_SCOPE_SUBTREE setting causes the search to encompass all branches in the tree below the point from which the search is being executed.

PADS_SEARCHPREF_INFO arSearchPrefs[2];

arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [0].vValue.Integer = 100;

arSearchPrefs [1].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
arSearchPrefs [1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [1].vValue.Integer = ADS_SCOPE_SUBTREE;

hr = pDSearch->SetSearchPreference(arSearchPrefs, 2);


Searching with ADO in Visual Basic

In order for ADO to communicate with ADSI, you need to have at least three ADO objects: a Connection, a Command, and a RecordSet. These ADO objects serve to authenticate users, initiate queries, and enumerate results, respectively. The Connection object loads the OLE DB provider, and validates user credentials. In Visual Basic®, you call CreateObject("ADODB.Connection") to create an instance of a Connection object, and then set the Provider property of the Connection object to "ADsDSOObject". "ADODB.Connection" is the ProgID of the Connection object and "ADsDSOObject" is the name of the OLE DB provider in ADSI. If no credentials are specified, the credentials of the currently logged on user are assumed.

The following code fragment illustrates how to create an instance of a Connection object using VBScript.

Dim Con As New Connection

Set con = CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"

The next example is an equivalent Active Server Page (ASP) code snippet.

<%
Set con = Server.CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"
%>

The second ADO object is the Command object. The ProgID is "ADODB.Command". This object lets you issue query statements and other commands to ADSI using the active connection. The Command object uses its ActiveConnection property to maintain an active connection. It also maintains the CommandText property to hold query statements issued by a user. The query statements are expressed in either the SQL dialect or the LDAP dialect. The following code snippets illustrate how to create a Command object.

The following VBScript Example uses the SQL query syntax to find all users whose last name is equal to Johnson from the global catalog service:

Dim command As New Command

Set command = CreateObject("ADODB.Command")
Set command.ActiveConnection = con
command.CommandText =
"SELECT AdsPath, cn, FROM '<LDAP://DC=ArcadiaBay,DC=com>'
 WHERE objectCategory='person' AND objectClass='user' AND sn = 'Johnson' ORDER BY sn

Alternatively, the LDAP syntax can be used:

Set command.ActiveConnection = con
command.CommandText =
     "<LDAP://DC=ArcadiaBay,DC=com(&(objectClategory=person)(objectClass=user)(sn=Johnson));AdsPath, cn; subTree"

As discussed earlier, you can set search preferences by setting values within the properties collection. Acceptable properties are listed in the following table.

Command Property | Type | Default Value
SearchScope | Enumeration | ADS_SCOPE_SUBTREE
Timeout | Integer | No time-out
Page size | Integer | No page size
Time limit | Integer | No time limit
Chase Referrals | Enumeration | ADS_CHASE_REFERRALS_EXTERNAL
Cache Results | Boolean | True

The following code example illustrates how to set search options in Visual Basic.

Dim Com As New Command

Com.Properties("Page Size") = 100
Com.Properties("Timeout") = 30    'seconds
Com.Properties("searchscope") = ADS_SCOPE_ONELEVEL     'Define in ADS_SCOPEENUM
Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
Com.Properties("Cache Results") = False   'do not cache the result set

The third ADO object is RecordSet. You obtain this object when you invoke the Execute method on a Command object. The primary function of the RecordSet object is to enumerate the result set and obtain the data. The result set can contain values for attributes that allow both single or multiple values. Getting an attribute with a single value is straightforward, similar to getting the column value in the relational database (for example, Fields('name').Value). Getting an attribute with multiple values, however, is more challenging. In this case, the Field.Value is an array and you must check the lower and upper bound of the array, as illustrated in the example below.

Set rs = Com.Execute

For i = 0 To rs.Fields.Count - 1
  Debug.Print rs.Fields(i).Name, rs.Fields(i).Type
Next i

'--------------------------------------
' Navigate the record set
'----------------------------------------
rs.MoveFirst
lstResult.Clear      'Clear the user interface
While Not rs.EOF
For i = 0 To rs.Fields.Count - 1
    'For Multi Value attribute
     If rs.Fields(i).Type = adVariant And Not (IsNull(rs.Fields(i).Value)) Then
        Debug.Print rs.Fields(i).Name, " = "
        For j = LBound(rs.Fields(i).Value) To UBound(rs.Fields(i).Value)
            Debug.Print rs.Fields(i).Value(j), " # "
            lstResult.AddItem rs.Fields(i).Value(j)
        Next j
     Else
        'For Single Value attribute
         Debug.Print rs.Fields(i).Name, " = ", rs.Fields(i).Value
         lstResult.AddItem rs.Fields(i).Value
     End If
Next i
rs.MoveNext
Wend


Query Syntax for ADO

A dialect, as defined in ADSI, refers to the syntax used in a query statement. ADSI supports the following two dialects:

SQL Dialect

Derived from the Structured Query Language, it uses more human readable expressions to define query statements. SQL statements have the following syntax:

SELECT [ALL] select-list FROM 'ADsPath' [WHERE search-condition] [ORDER BY sort-list]

SELECT (which may be followed by ALL) and FROM are required keywords. WHERE and ORDER BY are optional keywords.

The following are examples of query statements of the SQL dialect in ADSI:

Query in SQL dialect Description
SELECT ADsPath, cn FROM 'LDAP://DC=ArcadiayBay,DC=COM' WHERE objectCategory='group'
Search for all the group objects.
SELECT ADsPath, cn FROM 'LDAP://OU=Sales, DC=ArcadiaBay,DC=COM' WHERE objectCategory='person' AND objectClass='user' AND sn = 'H*' ORDER BY sn
 Search for all the users whose Last Name starts with letter H.

LDAP Dialect

An LDAP dialect string consists of four parts separated by semicolons (;):

  1. Base Distinguished Name: For example, <LDAP://DC=ArcadiaBay,DC=COM>
  2. LDAP Search Filters: For example, (&(objectCategory=person)(objectClass=contact)(sn=jsmith))
  3. Attributes: Such as, ADsPath, department, and manager.
  4. Search Scope: base, one-level, or sub-tree.

The following is an example of the LDAP dialect in ADSI:

Query in LDAP dialect Description
"<LDAP://DC=ArcadiaBay,DC=com>;(objectClass=*);AdsPath, cn; subTree"
 Searches all the objects in the sub-tree.


Distributed Query

Since ADSI is an OLE DB provider, it can participate in Distributed Query, introduced in Microsoft® SQL Server 7.0. This brings up many interesting scenarios:

  • Joining Active Directory™ objects with SQL Server data.
  • Updating SQL data from Active Directory objects.
  • Three- or four-way joins with other OLE DB providers. For example, Indexing Service, SQL Server, and Active Directory.

To set up a distributed join, you must first set up ADSI mapping using the sp_addlinkedserver stored procedure in a SQL Server Tool such as SQL Query Analyzer. This procedure links a name to an OLE DB provider's name.

For example,

sp_addlinkedserver 'ADSI', 'Active Directory Services 2.0', 'ADSDSOObject', 'adsdatasource'

Optionally, you can use sp_addlinkedsrvlogin to specify what the user should be run as. Since SQL 7.0 participates in Single Sign On (SSO), Kerberos delegation can be used when Kerberos is the chosen authentication package. If you elect to use Kerberos you should not run this option.

Example of an sp_addlinkedsrvlogin call:

sp_addlinkedsrvlogin ADSI, false, 'REDMOND\Administrator', 'CN=Administrator,OU=Sales, DC=actived,DC=microsoft,DC=com', 'secret**2000'

Once you have established this, you can start running the query against directory.

For example:

select  Name, sn lastName, street Street, l City, st State
from OpenQuery( ADSI,
'SELECT Name,sn, street, l, st, description FROM ''LDAP://OU=Sales, DC=actived,DC=Microsoft,DC=Com'' WHERE objectCategory=''person'' AND objectClass=''contact''')


Modifying an ADSI Object from ADO

The current version of ADSI ships with a read-only OLE DB provider. This means that you cannot issue the UPDATE statement in the SQL dialect at present. If you want to modify an object returned from an ADO query, you must perform the following three steps:

  1. Ask for the ADsPath when specifying attribute names, as in "SELECT ADsPath, …"
  2. Execute the query and get the ADsPath attribute.
  3. Bind to the record set using ADsPath, and modify the attributes.

The following code example illustrates how to modify an ADSI object after performing the steps outlined in the preceding example.

'Replacing department for all users in OU=sales
Set con = Server.CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"

Set command = CreateObject("ADODB.Command")
Set command.ActiveConnection = con

command.CommandText =
"SELECT AdsPath, cn, FROM '<LDAP://OU=Sales,DC=ArcadiaBay,DC=com>'
 WHERE objectClass = 'user'"

Com.Properties("searchscope") = ADS_SCOPE_ONELEVEL
Set rs = Com.Execute
While Not rs.EOF
  Set usr = GetObject(rs.Fields('AdsPath').Value)
  usr.Put "department", "1001"
  usr.SetInfo
  rs.MoveNext
Wend


Reading Properties for an Active Directory Object

All objects have properties. All ADSI COM objects have one or more interfaces with methods that retrieve the properties of the directory object that the COM object represents. There are a number of ways you can read properties from an object:

  • Get a specific property by name. The IADs interface has two methods IADs::Get and IADs::GetEx to read a specific property. Every ADSI COM object has an IADs interface.
  • Get a specified list of properties. The IDirectoryObject interface has the method IDirectoryObject::GetObjectAttributes that allows you to specify a list containing the names of the properties to read and returns an array of structures containing the requested property values.
  • Enumerate all properties on the object. The IADsPropertyList interface allows you to enumerate all the properties on an object.
  • Get special properties. The Automation interfaces (IADs*) have property methods that allow you to get special properties that are not stored in an object. Or the property methods may allow you to get an object property in a data format that differs from the actual data type stored. For example, the IADs interface has property methods such as IADs::get_Name which is an object's relative path (of the form RDNAttributeName=RDN, for example, CN=James Smith), IADs::get_Class which is an object's class, IADs::get_Parent which is the ADsPath to the object's parent.

ADSI allows you to cache properties locally after they have been read from the directory server. So, you also have a choice of reading the properties from the local property cache or retrieving the properties directly from the directory server. ADSI also has methods to update the cache as well as specifying whether all properties for an object are cached or just those you've specified.

After you have retrieved a property, you read its value. Active Directory™ stores a property's value as a specific data type. The properties of a property are defined in a schemaAttribute object. A schemaAttribute object contains properties that define the attribute syntax. The attribute syntax defines a property's data type.

The Automation interfaces (IADs*) return a property value as a VARIANT or a pointer to an Automation interface on a COM object that represents the property. The IDirectoryObject and IDirectorySearch interfaces return a property as a pointer to a structure containing a typed property value or a pointer to a string of bytes. In addition, IDirectoryObject and IDirectorySearch retrieve properties directly from the directory server instead of using a local property cache.


Property Cache

The ADSI object model provides a client-side property cache for each ADSI object. Think of the cache as a table in memory that contains the names and values of all an object's properties. When an object is created, its property cache is empty. Calling IADs::GetInfo loads the object's properties from the underlying directory service. If the cache is empty, calling get or get_propertymethod from a client written in C or C++, or calling object.propertymethod from an Automation client, forces an implicit GetInfo. Once the cache is filled, all Get calls work on the contents of the cache only.


Getting Properties

This section discusses the following:

  • Get Method
  • GetEx Method
  • GetInfo Method
  • Optimization Using GetInfoEx
  • Getting Properties with the IDirectoryObject Interface


Get Method

Individual properties can be retrieved from the directory using the IADs::Get method.

The following example retrieves a property by name from an object using IADs::Get.

Dim MyUser as IADs
Dim MyCommonName as String

' Bind to a specific user object.
set MyUser = GetObject("LDAP://CN=JamesSmith,OU=MyOrgUnit")

' Get property
MyCommonName = MyUser.Get("CN")

From Automation languages, you can use the property name directly using the dot notation. For example, you can write MyObject.Name or MyObject.Class to access the Name and Class properties defined on IADs.

The following example retrieves a property directly from an Active Directory object using the standard IADs interface and the Name property it supports.

Dim MyUser as IADs
Dim MyName as String

' Bind to a specific user object.
Set MyUser = GetObject("LDAP://MyMachine/CN=JamesSmith,DC=ArcadiaBay,DC=COM")

' Get property
MyName = MyUser.Name

You can also use the name of the schema object that describes the property. Pass the name of the property as it is defined in the schema as the first parameter.

Dim MyUser as IADs
Dim MyName as String

' Bind to a specific user object.
set MyUser = GetObject("LDAP://CN=JamesSmith,OU=MyOrgUnit")

' Get property
MyName = MyUser.Get("distinguishedName")

From languages that do not use Automation, you can also use the IADs::Get method and its related methods as described previously. In addition, you can use the get_propertyname methods supported on all ADSI interfaces.

IADs       *pUser;
BSTR       bstrName;
HRESULT    hr;

// Bind to user object
hr = ADsGetObject(L"WinNT://MyDomain/Users/James", IID_IADs, (void**)&pUser);

// Get property
if (SUCCEEDED(hr)) {
    hr = pUser->get_Name(&bstrName);

    if (SUCCEEDED(hr)) {
        printf("%S\n", bstrName);
    }

    SysFreeString(bstrName)
}


GetEx Method

Some properties are returned as multi-valued. They can contain one or more values. For instance, a list of descriptions on a domain is a multi-valued property. You can retrieve a multi-valued property as an array using the IADs::GetEx method.

IADs::GetEx gets properties that support single or multiple values in VARIANT structures from the property cache. Finding no value in the cache invokes an implicit IADs::GetInfoEx. IADs::Get can also be used to retrieve properties with a single value.


Example Code for Using GetEx

[Visual Basic]

Dim obj As IADs
Dim objList As Variant

Set obj = GetObject("LDAP://MyMachine/CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")

objList = obj.GetEx("description")

For Each Desc In objList
    ' Print the descriptions
    Debug.Print (Desc)
Next


GetInfo Method

Call the GetInfo method to refresh all of an ADSI object's cached properties from the underlying directory service. To refresh specific properties, use the GetInfoEx method.

ADSI invokes an implicit GetInfo if a Get is performed on a specific property in the property cache and no value is found. Once GetInfo has been called, an implicit call will not be repeated. If a value already exists in the property cache, however, calling Get without first calling GetInfo will retrieve the cached value rather than the most current value from the underlying directory. To obtain the most recent values for an object, always call GetInfo. Any changes you have made in the property cache will be replaced with the current values from the server. If you need to preserve your changes on the server you should do a SetInfo to save your changes before you do a GetInfo.


Example Code for Using GetInfo

[Visual Basic]

Dim MyUser as IADsUser
'MyUser will be used to demonstrate implicit GetInfo
Dim MyUser2 as IADsUser
'Myuser2 will show the explicit GetInfo

' Bind to a specific user object.
set MyUser = GetObject("LDAP://MyMachine/CN=JamesSmith,DC=ArcadiaBay,DC=COM")
set MyUser2 = GetObject("LDAP//MyMachine/CN=JamesSmith2,DC=ArcadiaBay,DC=COM");

'Perform some time consuming operations.

'Code assumes that the property description has a single value in the directory
' Note that this will IMPLICITLY call GetInfo as at the point this call is made GetInfo
' has not yet been called (implicitly or explicitly) on the MyUser object.
Debug.print "MyUser's description value is "; MyUser.Get("Description")

'Since the GetInfo has already been called implicitly this call is satisfied from
' the value in the cache.
Debug.print "MyUser's sAMAccountName is "; MyUser.Get("sAMAccountName")

' Refresh the cache explicitly so the most current value is available
MyUser2.GetInfo

'Perform time consuming operations

'Note that this call is satisfied from the cache has GetInfo has already been called
'explicitly for this object.
Debug.print "MyUser2 has the description set to "; MyUser.Get("Description")


Optimization Using GetInfoEx

The GetInfoEx method is called explicitly to refresh some of an ADSI object's cached properties from the underlying namespace. To refresh all the properties, use GetInfo.

GetInfoEx gets specific current values for the properties of an Active Directory object from the underlying directory store, refreshing the cached values.


Example Code for Using GetInfoEx

The following Visual Basic code sample shows how to use GetInfoEx.

Calling GetInfoEx after changing property values but before calling SetInfo results in losing those changed values. After this call, the cache reflects the property values in the underlying namespace directory store. However, the cache is only updated with the values specifically requested in the GetInfoEx call. Some servers will not return all properties of an object in response to a GetInfo call, you must do an explicit GetInfoEx call naming these "non-default" properties in order to get them into the cache.

Dim PropArray As Variant
Dim Prop As Variant
Dim DescList As Variant
Dim obj As IADs

Set obj = GetObject("LDAP://MyMachine")

' Initialize the array of properties to pass to GetInfoEx
PropArray = Array("description", "distinguishedName")

' Make the array a single variant for passing to GetInfoEx
Prop = PropArray

' Get just the description and DN properties
obj.GetInfoEx Prop, 0

DescList = obj.Get("description")

' Enumerate the descriptions
For Each Desc In DescList
    ' Print the descriptions
    Deubg.Print (Desc)
Next


Getting Properties with the IDirectoryObject Interface

The IDirectoryObject interface provides clients written in C and C++ with direct access to directory service objects. The interface uses a direct on-the-wire protocol to provide access, rather than using the ADSI property cache. In place of the properties supported by the IADs interface, IDirectoryObject provides methods that support a critical subset of an object's housekeeping methods and provides access to its attributes. With IDirectoryObject, a client can get or set any number of object attributes with one method call. Unlike the corresponding Automation methods, which are batched, those of IDirectoryObject are executed as soon as they are called. Since methods on this interface do not require creating an instance of an Automation directory object, the performance overhead is very low.

Clients written in languages such as C and C++ should call the methods of IDirectoryObject to optimize performance and take full advantage of native directory service interfaces. Automation clients cannot use IDirectoryObject. Instead, they should call the IADs interface.


Example Code for IDirectoryObject

[C++]

// IDirObj.cpp : Demonstrates the use of IDirectoryObject.
//

#define _WIN32_DCOM
#define UNICODE
#define _UNICODE

#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "activeds.h"
#include "tchar.h"

void main(void)
{
    IDirectoryObject    *pDirectoryObj;
    DWORD                dwNumAttrToGet;
    DWORD                dwNumAttrGot;
    LPWSTR               AttribNames[10];
    ADS_ATTR_INFO       *pPropEntries;
    HRESULT              hr;

    // Call to initialize COM Library
    CoInitialize(NULL);

    // Bind to a known object asking for the IDirectoryObject Interface.
    hr = ADsOpenObject( L"LDAP://MyMachine/CN=Administrator,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=COM",
                L"Administrator",
                L"",
                ADS_SECURE_AUTHENTICATION,
                IID_IDirectoryObject,
                (void**)&pDirectoryObj);

    if(FAILED(hr)) {
        // Error code goes here.
        printf("ADsOpenObject Failed\n");
        exit(1);
    }

    // Create a list of attributes to get.
    AttribNames[0] = L"distinguishedName";
    dwNumAttrToGet = 1;

    // Call GetObjectAttributes with the
    // list of attributes and a pointer to
    // an array of ADS_ATTRIB_INFO structs.
    hr = pDirectoryObj->GetObjectAttributes( AttribNames,
                                             dwNumAttrToGet,
                                             &pPropEntries,
                                             &dwNumAttrGot);
    if(FAILED(hr)) {
        // Error code goes here.
        pDirectoryObj->Release();
        printf("IDirectoryObject::GetObjectAttributes Failed\n");
        exit(1);
    }

    printf("distinguishedName: %S\n",
            pPropEntries[0].pADsValues->PrintableString);

    // Cleanup.
    pDirectoryObj->Release();
    FreeADsMem(pPropEntries);

    // Call to uninitialize COM Library
    CoUninitialize();

}


Setting Properties

Setting properties is very similar to getting properties. However, when setting a property, the value is only written to the local property cache on the client machine until the SetInfo method is called. SetInfo is always an explicit call, it is never called implicitly.

You can change the property values in the property cache as necessary for your application using IADs methods or IADsPropertyList methods, then write the changes back out to the directory service when they are in a consistent state using SetInfo.

This section discusses the following:

  • Put Method
  • PutEx Method
  • SetInfo Method


Put Method

The Put method saves the value for a property for an Active Directory object by name into the property cache. Use the IADs::PutEx method to save properties with multiple values to the property cache or to remove a property from an object. These values are not persisted to the underlying directory service until IADs::SetInfo is called.


Example Code for Using Put

[Visual Basic]

Dim Namespace As IADsOpenDSObject
Dim User As IADsUser
Dim NewName As Variant

Set Namespace = GetObject("LDAP:")

Set User = Namespace.OpenDSObject("LDAP://MyMachine/CN=Administrator,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=COM", "Administrator", "", ADS_SECURE_AUTHENTICATION)

NewName = "James Smith"

' Set using IADs::PutMethod
User.Put "FullName", NewName
User.SetInfo


PutEx Method

The PutEx method uses the name of a property to save a single or multi-valued property into the property cache. This overwrites any value currently in the property cache. The values in the cache are not written to the underlying directory service until an IADs::SetInfo occurs. The first argument of PutEx indicates whether you want to replace or add to any existing values for the property. In the following example, any existing values of the description attribute would be erased in the cache when PutEx is called and erased on the server when SetInfo is called.


Example Code for Using PutEx

[Visual Basic]

Dim x As IADs
Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")
'----------------------------------------------
' Assume the otherHomePhoneNumber has the following values:
' 111-1111, 222-2222
'----------------------------------------------
x.PutEx ADS_PROPERTY_APPEND, "OtherhomePhone", Array("333-3333" )
x.SetInfo              'Now the values are 111-1111,222-222,333-3333

x.PutEx ADS_PROPERTY_DELETE, "OtherHomePhone", Array("111-1111", "222-2222")
x.SetInfo              'Now the values are 333-3333
x.PutEx ADS_PROPERTY_UPDATE, "OtherHomePhone", Array("888-8888", "999-9999")
x.SetInfo              'Now the values are 888-8888,999-9999
x.PutEx ADS_PROPERTY_CLEAR, "OtherHomePhone",  vbNull
x.SetInfo              'Now the property has no value


SetInfo Method

The SetInfo method saves the current values for the properties for this Active Directory object from the property cache to the underlying directory store. This is analogous to flushing a buffer out to disk.

SetInfo will update objects that already exist in the directory or create a new directory entry for newly created objects.

At the time of the SetInfo call, if any property cache values have been written with a PutEx control code such as ADS_PROPERTY_UPDATE or ADS_PROPERTY_CLEAR, then the appropriate requests are passed on to the underlying directory service.


Enumerating Properties

The IADsPropertyList interface allows you to enumerate all the properties on an object.


Providing Direct Access to the Property Cache

In addition to the IADs methods that get and set individual properties in the cache, ADSI provides the IADsPropertyList, IADsPropertyEntry, and IADsPropertyValue interfaces that allow client software to access the property cache directly. These interfaces allow you to work with the properties without having to use the individual property names.

Interface | Purpose
IADsPropertyList | Manage the entire list of properties defined on an object.
IADsPropertyEntry | Manage individual property attributes and values.
IADsPropertyValue | Read and write values for each property entry in the property cache.

The ADSI property-cache interfaces represent each property entry. IADsPropertyEntry allows access to a single property in the cache and includes a pointer to the list of IADsPropertyValue pointers needed to represent each value.

The IADsPropertyList interface manages a property list node in a namespace. With this interface, you can enumerate the properties in the list, add and remove properties, or purge the entire list in one step.

Dim propList As IADsPropertyList
Dim propEntry As IADsPropertyEntry
Dim propValue As IADsPropertyValue
Dim rootDSE As IADs

'Get the rootDSE entry that contains configuration information
Set rootDSE = GetObject("LDAP://RootDSE")

'Bind to the domain
Set propList = GetObject("LDAP://" & rootDSE.Get("defaultNamingContext"))

'Get the Property Entries

Set propEntry = propList.GetPropertyItem("allowedChildClassesEffective", ADSTYPE_CASE_IGNORE_STRING)

'Get more info about Property Entry
Debug.Print propEntry.ADsType
Debug.Print propEntry.ControlCode
Debug.Print propList.PropertyCount

'Values contain pointers to IADsPropertyValues interfaces
For Each v In propEntry.Values
    Set propValue = v
    Debug.Print propValue.CaseIgnoreString
Next


Extending the User Interface for Directory Objects

Windows® 2000 ships with Microsoft Management Console (MMC) snap-ins for administration of Active Directory such as the specifically Active Directory Users and Computers snap-in. In addition, the Windows 2000 client includes user interface (UI) for finding objects that reside in the directory and reading and writing properties. This section details what you need to know to extend the user interface (UI) for viewing and managing Active Directory objects in the Windows shell and Active Directory administrative snap-ins. This section also covers what you need to do to make it easy for your customers to deploy the UI extensions to the user's desktops.

Specifically, this section discusses the following:

  • How to extend the UI for Active Directory objects.
  • How to use display specifiers, and where to store display specifiers.
  • How to add property pages to the property sheets of objects of a specific Active Directory object class.
  • How to add menu items to the context menus of objects of a specific Active Directory object class.
  • How to set the display name used for an object of a specific Active Directory object class and the display names of its attributes. For example, suppose you wanted to change what is displayed in the UI for the attribute l (city) which is specified in the schema documentation in the Platform SDK to represent what city a person is located in. You can set an attribute display name to map "City" to the l attribute. The user class's display name is "User" Thus, when the text "User" appears in the New menu for creating a new user, and the text "City" is the label for the edit box that represents the l property in the Address tab of a user's property sheet.
  • How to set the icon used to represent objects of a specific Active Directory class. The icon is displayed in the Windows shell and Active Directory Administrative snap-ins.
  • How to specify whether objects of a specific Active Directory class are displayed as containers or leaf objects. If an object is a leaf object, it only appears in the result (right) pane of the administrative snap-ins and Explorer view of the Windows shell. If an object is a container, it can appear in either the scope (left) or result pane.
  • How to extend or replace an existing creation wizard for objects of a specific Active Directory class. Creation wizards simplify what a user needs to enter in order for the object to be created. Currently, the user, computer, group, and other Active Directory classes have creation wizards. This functionality will be available after Beta 3.
  • How to take advantage of the standard Windows 2000 query and selection UI. The Windows 2000 operating system provides COM objects that implement dialog boxes for browsing and selecting the following directory objects: domains, containers, and specific types of objects. In addition, the query (Find) dialog box can also be used or extended (Post-Beta 3).
  • How to provide user interfaces for new object classes.
  • How to extend Active Directory Administrative Snap-ins using MMC extension snap-ins.
  • How to use the Microsoft Installer (MSI) and Windows 2000 application deployment to distrubute the COM objects created to perform the tasks listed above.


About Active Directory User Interfaces

Both administrators and users must be able to view Active Directory objects in the user interface.

Administrators will manage Active Directory using different Microsoft Management Console (MMC) snap-ins — specifically Active Directory Users and Computers, Active Directory Sites and Services, Active Directory Domains and Trusts, and Active Directory Schema Manager.

The end user, however, will see the directory through the Windows shell. Users can browse for objects stored in the directory from either the Directory item in My Network Places on the Desktop or through the Find dialogs available in the Start menu.

Active Directory supports a user interface (UI) that adapts to meet the needs of administrators and end users. Active Directory enables you to extend the user interface that represents existing object classes as well as new classes added to the schema. You can control or extend the following UI elements for each class defined in the schema:

  • Property pages
  • Context menus
  • Class display name
  • Object creation wizard (Post-Beta 3).
  • Attribute display names
  • Class icons
  • Containers viewed as leaf nodes

In addition, the Windows 2000 operating system provides COM objects that implement common dialog boxes for handling directory objects:

  • Directory Object Picker (for specific types)
  • Domain Browser
  • Container Browser
  • Query Forms (Post-Beta 3)

Note that your application can use these common dialog boxes to browse for, select, or query for directory objects instead of having to implement your own dialog boxes to perform these common actions.

For Active Directory administration snap-ins, context menus and property pages can also be extended using MMC extension snap-ins. You can also implement the other MMC extensions as well: taskpads, namespace items, control bars, and toolbars. For more information, see Extending Active Directory Administrative Snap-ins using MMC Extension Snap-ins.


Display Specifiers

User interface information is stored in Active Directory. This UI specification is done at the granularity of each object class. An object class (or class) defines a type of object that can be created in Active Directory. Classes are stored as classSchema objects in the schema. Each classSchema object can have UI display information associated uniquely with it. For more information about the classes, classSchema objects, and extending the Schema, see Extending the Schema.

Contain UI display information

The UI display information for a class is stored in an Active Directory object called a display specifier (an object of class displaySpecifier). Each displaySpecifier object contains attributes describing the various UI elements for a particular object class. Display specifiers store information for property sheets, context menus, icons, creation wizards, and localized class and attribute names. For property pages and context menus, the Windows shell and Active Directory administrative snap-ins use this information to form different user interfaces for administrators and end users — one set of property pages and/or context menus can be associated with administrative applications while a different set of elements can be associated with end user applications. You can add, delete, or modify properties of a class's displaySpecifier object to specify the UI elements (class name, attribute names, property sheets, context menus, icon, and so on) that appear for each instance of an object of that class.

Locale of display specifiers

In addition, a display specifier is specific to a particular locale based on the container that it is in.

Within the DisplaySpecifiers container in the Configuration container, there are containers that correspond to each locale.

Naming display specifiers

Display-Specifier objects are named by appending the string "–Display" to the LDAP Display Name of the class object.

For example, the user class has a corresponding Display-Specifier object called "user-Display." Thus, when you bind to an object of a particular class, you look up the Display-Specifier object based on the same name as the class and within the container for the current locale.

The following example shows the values set on the group-Display display specifier object:

[group-Display]
objectClass = displaySpecifier
ObjectCategory = Display-Specifier
cn = group-Display
adminPropertyPages = 1,{6dfe6489-a212-11d0-bcd5-00c04fd8d5b6}
adminPropertyPages = 2,{6dfe648b-a212-11d0-bcd5-00c04fd8d5b6}
adminPropertyPages = 3,{6dfe6488-a212-11d0-bcd5-00c04fd8d5b6}
adminPropertyPages = 4,{4E40F770-369C-11d0-8922-00A024AB2DBB}
shellPropertyPages = 1,{f5d121ee-c8ac-11d0-bcdb-00c04fd8d5b6}
shellPropertyPages = 2,{dde2c5e9-c8ae-11d0-bcdb-00c04fd8d5b6}
contextMenu = 0,{62AE1F9A-126A-11D0-A14B-0800361B1103}
adminContextMenu = 1,{08eb4fa6-6ffd-11d1-b0e0-00c04fd8dca6}
classDisplayName = Group
attributeDisplayNames = cn,Name
attributeDisplayNames = c,Country Abbreviation
attributeDisplayNames = description,Description
attributeDisplayNames = distinguishedName,X500 Distinguished Name
attributeDisplayNames = l,City
attributeDisplayNames = managedBy,Managed By
attributeDisplayNames = member,Members
attributeDisplayNames = notes,Notes
attributeDisplayNames = physicalDeliveryOfficeName,Delivery Office
attributeDisplayNames = url,Web Page Address
treatAsLeaf=True


DisplaySpecifiers Container

Display specifiers are stored by locale in the DisplaySpecifiers container within the Configuration container. Because the Configuration container is replicated across the entire forest, display specifiers are propagated across all domains in a forest.

The Configuration container stores the DisplaySpecifiers container, which in turn stores containers that correspond to each locale. These locale containers are named using the hexidecimal representation of that locale's LCID. Thus the US/English locale's container is named 409, the German locale's container is named 407, the Japanese locale's container is named 411, and so forth.

Each locale container stores objects of the class Display-Specifier.

To list all display specifiers, simply enumerate all the displaySpecifier objects in the specified locale container within the DisplaySpecifiers container.

The following code fragment contains a function that binds to the display specifier container for the specified locale:

//This function returns a pointer to the display specifier container
//for the specified locale.
//If locale is NULL, use default system locale and then return the locale in the locale param.
HRESULT BindToDisplaySpecifiersContainerByLocale(LCID *locale,
                                        IADs **ppDispSpecCont
                                          )
{
HRESULT hr = E_FAIL;

if ((!ppDispSpecCont)||(!locale))
  return E_POINTER;

//if no locale is specified, use the default system locale.
if (!(*locale))
    {
    *locale = GetSystemDefaultLCID();
    if (!(*locale))
    return E_FAIL;
    }

//Make sure that it's a valid locale.
if (!IsValidLocale(*locale, LCID_SUPPORTED))
    return E_INVALIDARG;

LPOLESTR szPath = new OLECHAR[MAX_PATH*2];
IADs *pObj = NULL;
VARIANT var;

hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)&pObj);

if (SUCCEEDED(hr))
    {
    //Get the DN to the config container.
    hr = pObj->Get(L"configurationNamingContext",&var);
    if (SUCCEEDED(hr))
        {
        //Build the string to bind to the DisplaySpecifiers container.
        wsprintf(szPath,L"LDAP://cn=%x,cn=DisplaySpecifiers,%s", *locale, var.bstrVal);
        //Bind to the DisplaySpecifiers container.
        *ppDispSpecCont = NULL;
        hr = ADsOpenObject(szPath,
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)ppDispSpecCont);

        if(FAILED(hr))
            {
                if (!(*ppDispSpecCont))
                {
                    (*ppDispSpecCont)->Release();
                    (*ppDispSpecCont) = NULL;
                }
            }
        }
    }
//Clean up
VariantClear(&var);
if (pObj)
    pObj->Release();

return hr;
}


Property Pages for Use with Display Specifiers

You can add property pages to the existing property sheet for a particular object class within the Windows shell or Active Directory administrative snap-ins. Property pages are implemented as COM objects. For example, you can add a property page to the property sheet for objects of the contact class. When a user views a contact object, your property page is displayed with the rest of the property pages in the contact property sheet.

To extend property sheets with custom property pages

  1. Write the property page COM object. The property page COM object must implement IShellExtInit and IShellPropSheetExt.
  2. Install the property page COM object on the computers where you want the extension property pages to be used. To do this, create a Microsoft Installer package for your property page COM object's DLL and deploy the package appropriately using the group policy. See Using MSI and Windows 2000 Application Deployment to Distribute UI Additions.
  3. Register the property page extension for a particular object class so that the property page is added to the property sheets of objects of that class.

    For the Windows shell or Active Directory administrative snap-ins to recognize your property page, you must add a value for your property page COM object in the adminPropertyPages and/or shellPropertyPages property of the display specifier for the class whose property sheet you want to extend. Note that you can register the same property page COM object for more than one class.

    Note that registration is per locale. If the property page COM object is specific to a certain locale, it should be registered in a displaySpecifier in the locale container that it applies to. If it applies to all locales, it should be added to the appropriate displaySpecifier (that is, the one that represents the class that should get the property page extension) in all locale containers.

The following sections show these steps.


Implementing the Property Page COM Object

To extend property sheets for directory objects, a property page COM object must implement two shell interfaces: IShellExtInit and IShellPropSheetExt.

A property page COM object is instantiated when the user views the properties for an object of a class for which the property page has been registered. If the COM object was registered in the class's adminPropertyPages property, the property page appears when the user views properties for objects of that class in Active Directory administrative snap-ins. If it was registered in shellPropertyPages, the property page appears in the Windows shell.

After the property page COM object is instantiated, the IShellExtInit::Initialize method is called. The IShellExtInit interface has only one method and it must be implemented:

IShellExtInit::Initialize
This method is called by the immediately after the property page COM object has been instantiated. This is the property page COM object's only opportunity to get the IDataObject pointer that enables the object to get information about the selected objects. For more information about IDataObject and data transfer, see the COM section of the Platform SDK. You should store the IDataObject pointer in a member variable so you can use the IDataObject::GetData method to retrieve information with the following clipboard formats:
CFSTR_DS_DISPLAY_SPEC_OPTIONS
Provides information about whether the property page was invoked by the Windows shell or by an Active Directory administrative snap-in. The data is returned as an HGLOBAL that points to a DSDISPLAYSPECOPTIONS structure. The structure contains additional information that tells you about the user context and server where the shell or snap-in retrieved the data about the selected objects. This additional information is useful if you need to bind to one or more objects in the selection.
CFSTR_DSOBJECTNAMES
Provides the list of objects that were selected when the property page was invoked by the Windows shell or by an Active Directory administrative snap-in. The data is returned as an HGLOBAL that points to a DSOBJECTNAMES structure. The structure contains the count of items in the selection and a pointer to an array of DSOBJECT structures that represent each selected item. The DSOBJECT structure contains the ADsPath, class name, and flags (indicating whether the page should be read-only and whether the object is a container). If it is necessary to read or modify properties on the object, the ADsPath can be used to bind to the object and perform the necessary operations.

You should also store the clipboard formats as member variables. The IShellExtInit::Initialize method is a good place to register these formats.

CFSTR_DSPROPERTYPAGEINFO
Applies only to property pages. This clipboard format provides the optionaldata string at the end of property page COM object's value in the adminPropertyPages and shellPropertyPages property. If optionaldata was not specified, IDataObject::GetData will return a E_NOTIMPL result.

The following code fragment contains an implementation of the IShellExtInit::Initialize method. This method releases the IDataObject pointer if IShellExtInit::Initialize was called before, stores the passed IDataObject pointer in a member variable, and registers the two clipboard formats in a member variable:

STDMETHODIMP CMyDsPropertyPage::Initialize(LPCITEMIDLIST pIDFolder,
                                   LPDATAOBJECT pDataObj,
                                   HKEY hRegKey)
{
    // Initialize can be called more than once.
    if (m_pDataObj)
        m_pDataObj->Release();

    // Keep a pointer to the IDataObject.
    if (pDataObj)
    {
        m_pDataObj = pDataObj;
        pDataObj->AddRef();
    }

    //Register the clipboard formats to use for getting info about
    //the selected DS objects.
    m_cfDsDispSpecOptions = RegisterClipboardFormat(CFSTR_DS_DISPLAY_SPEC_OPTIONS);
    m_cfDsObjectNames = RegisterClipboardFormat(CFSTR_DSOBJECTNAMES);

    return NOERROR;
}

Note that the IShellExtInit::Initialize method is implemented the same way for context menu extensions.

After IShellExtInit::Initialize returns, the IShellPropSheetExt::AddPages method is called.

You can implement the property page(s) in the class implementing the IShellPropSheetExt and IShellExtInit interfaces. Or you can implement each property page in a class that is completely separate from the class implementing the two shell extension interfaces.

The class that implements the two shell extension interfaces only needs to be instantiated for the creation of the pages, whereas the class for each page lasts as long as the property sheet. By implementing a class for each page, the class can be tailored for manipulating the attributes displayed on its page. If the property pages are implemented as separate classes, the class implementing the property page should be initialized with the appropriate member data such as the IDataObject pointer and any other data about the selected objects required by the property page class.

The IShellPropSheetExt interface has two methods that must be implemented:

IShellPropSheetExt::AddPages
This method is called just before the property sheet is displayed. This is the property page COM object's opportunity to create one or more property pages using the CreatePropertySheetPage function.In the PROPSHEETPAGE structure passed to CreatePropertySheetPage, store a this pointer to the class that owns the property page in the lParam member, so that the dialog box procedure for the property page can access the class members. When the dialog box procedure for the property page is called, the pointer to the PROPSHEETPAGE structure is passed as the lParam. This means the dialog box procedure has access to the class members.

Important The IShellPropSheetExt::AddPages method call runs on the caller's main thread. The dialog box procedure runs on a different thread. Therefore, do not reference the IDataObject pointer in the dialog box procedure unless it is first marshalled to this thread.

If the dialog box procedure requires data from the IDataObject, you can avoiding having to marshal the IDataObject pointer by extracting the data that the dialog box procedure needs and storing that data in member variables in the class. This can be done in the IShellExtInit::Initialize or the IShellPropSheetExt::AddPages methods. Then the dialog box procedure can access the class members via the lParam. Typically, the dialog box procedure will need only the ADsPath of the selected objects so that it can then use the ADsPath information to bind and perform operations on those objects.

IShellPropSheetExt::ReplacePage
This method is not called. It is only called for extension pages for the Control Panel. The method should just return E_FAIL.

The following code fragment contains an implementation of the IShellPropSheetExt methods and the dialog box procedure for the single property page added by the IShellPropSheetExt::AddPages method. The property page binds to the selected object and displays some properties for it. For the full implementation of this sample, see the following DSSamplePage sample.

STDMETHODIMP DSSamplePage::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPage, LPARAM lParam)
{
    HRESULT hr = S_OK;
    STGMEDIUM ObjMedium = {TYMED_NULL};
    FORMATETC fmte = {g_cfDsObjectNames, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
    LPDSOBJECTNAMES pDsObjectNames;
    PWSTR pwzObjName;
    PWSTR pwzClass;

    // Get the path to the DS object from the data object.
    // Note: This call runs on the caller's main thread. The pages' window
    // procs run on a different thread, so don't reference the data object
    // from a winproc unless it is first marshaled on this thread.
    // For this sample, we extract the data we want and store
    // it in member variables so that the dialog proc
    // can access the data without having to marshall
    // the IDataObject pointer.
    hr = m_pDataObj->GetData(&fmte, &ObjMedium);
    if (SUCCEEDED(hr))
    {
        pDsObjectNames = (LPDSOBJECTNAMES)ObjMedium.hGlobal;

        if (pDsObjectNames->cItems < 1)
        {
            hr = E_FAIL;
        }
        pwzObjName = (PWSTR)ByteOffset(pDsObjectNames,
                                       pDsObjectNames->aObjects[0].offsetName);
        pwzClass = (PWSTR)ByteOffset(pDsObjectNames,
                                     pDsObjectNames->aObjects[0].offsetClass);
        // Save the ADsPath of object
        m_ObjPath = new WCHAR [wcslen(pwzObjName )+1];
        wcscpy(m_ObjPath,pwzObjName);
    }

    // Now release the ObjMedium:
    // If punkForRelease is NULL, the receiver of
    // the medium is responsible for releasing it; otherwise,
    // punkForRelease points to the IUnknown on the appropriate
    // object so its Release method can be called.

    ReleaseStgMedium(&ObjMedium);

    PROPSHEETPAGE psp;
    HPROPSHEETPAGE hpage;
    HRESULT hres = 0;

    LPCSHELLEXT lpcsext = this;
    //
    // Create a property sheet page object from a dialog box.
    //
    // We store a pointer to our class in the psp.lParam, so we
    // can access our class members from within the DSSamplePageDlgProc.
    //
    // If the page needs more instance data, you can append
    // arbitrary size of data at the end of this structure,
    // and pass it to the CreatePropSheetPage. In such a case,
    // the size of entire data structure (including page specific
    // data) must be stored in the dwSize field.   Note that in
    // general you should NOT need to do this, as you can simply
    // store a pointer to data in the lParam member.
    //

    psp.dwSize      = sizeof(psp);    // no extra data.
    psp.dwFlags     =  PSP_USETITLE | PSP_USECALLBACK;
    psp.hInstance   = g_hInstance;
    psp.pszTemplate = MAKEINTRESOURCE(IDD_DSSamplePage);
    psp.hIcon       = 0;
    psp.pszTitle    = L"Object Stats";
    psp.pfnDlgProc  = DSSamplePageDlgProc;
    psp.pcRefParent = NULL;//&g_cRefThisDll;
    psp.pfnCallback = DSSamplePageCallback;
    psp.lParam      = (LPARAM)lpcsext;

    AddRef();
    hpage = CreatePropertySheetPage(&psp);

    if (hpage)
    {
        if (!lpfnAddPage(hpage, lParam))
        {
            DestroyPropertySheetPage(hpage);
            Release();
        }
    }

    return NOERROR;

}

STDMETHODIMP DSSamplePage::ReplacePage(UINT uPageID, LPFNADDPROPSHEETPAGE lpfnReplaceWith, LPARAM lParam)
{
    return E_FAIL;
}

BOOL CALLBACK DSSamplePageDlgProc(HWND hDlg,
                             UINT uMessage,
                             WPARAM wParam,
                             LPARAM lParam)
{
    LPPROPSHEETPAGE psp=(LPPROPSHEETPAGE)GetWindowLong(hDlg, DWL_USER);
    UINT iIndex=0;
    LPCSHELLEXT lpcs;

    switch (uMessage)
    {
        //
        // When the shell or snap-in creates a dialog box for a property sheet page,
        // it passes the pointer to the PROPSHEETPAGE data structure as
        // lParam. The dialog procedures of extensions typically store it
        // in the DWL_USER of the dialog box window.
        //
        case WM_INITDIALOG:
            {
                SetWindowLong(hDlg, DWL_USER, lParam);
                BSTR bsResult;

                psp = (LPPROPSHEETPAGE)lParam;

                lpcs = (LPCSHELLEXT)psp->lParam;

                HRESULT hr;
                IADs* pIADs = NULL;

                hr = ADsGetObject(  lpcs->m_ObjPath, IID_IADs,(void **)&pIADs);

                if (SUCCEEDED(hr))
                {
                    // Retrieves the GUID for this object- The guid uniquely identifies
                    // this directory object. The Guid is globally unique
                    // Also the guid is rename/move safe. The ADsPath below returns the
                    // CURRENT location of the object- The guid remains constant regardless of
                    // name or location of the directory object
                    pIADs->get_GUID(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_GUID),bsResult);
                    SysFreeString(bsResult);

                    // Retrieves the RDN
                    pIADs->get_Name(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_NAME),bsResult);
                    SysFreeString(bsResult);

                    // Retrieves the value in the class attribute, that is, group
                    pIADs->get_Class(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_CLASS),bsResult);
                    SysFreeString(bsResult);

                    // Retrieves the full literal LDAP path for this object.
                    // This may be used to re-bind to this object- though for persistent
                    // storage (and to be 'move\rename' safe) it is suggested that the
                    // guid be used instead of the ADsPath
                    pIADs->get_ADsPath(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_ADSPATH),bsResult);
                    SysFreeString(bsResult);

                    // Retrieves the LDAP path for the parent\container for this object
                    pIADs->get_Parent(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_PARENT),bsResult);
                    SysFreeString(bsResult);

                    // Retrieves the LDAP path for the Schema definition of the object returned from
                    /// the IADs::get_Schema() member
                    pIADs->get_Schema(&bsResult);
                    SetWindowText(GetDlgItem(hDlg,IDC_SCHEMA),bsResult);
                    SysFreeString(bsResult);

                    pIADs->Release();
                    pIADs = NULL;
                }
            }
            break;

        case WM_DESTROY:
            RemoveProp(hDlg, L"ID");
            break;

        case WM_NOTIFY:
            switch (((NMHDR FAR *)lParam)->code)
            {
                case PSN_SETACTIVE:
                    break;

                case PSN_APPLY:
                    //TODO: Apply changes user made here.
                     break;

                default:
                    break;
            }
            break;

        default:
            return FALSE;
    }

    return TRUE;
}


Registering the Property Page COM Object in a Display Specifier

Even after you've written and installed your property page COM object, the Windows shell and Active Directory administrative snap-ins cannot recognize it yet. For the Windows shell or Active Directory administrative snap-ins to recognize your property page, you must add a value for your property page COM object in the adminPropertyPages and/or shellPropertyPages property of the display specifier for the class whose property sheet you want to extend. Note that you can register the same property page COM object for more than one class.

If the COM object was registered in the adminPropertyPages property of the class's display specifier, the property page appears when the user views properties for objects of that class in Active Directory administrative snap-ins. If it was registered in shellPropertyPages, the property page appears in the Windows shell. If you want it to appear in both the shell and administrative snap-ins, you need to register the COM object in both properties.

The adminPropertyPages and shellPropertyPages properties are multi-valued. The adminPropertyPages contains the list of property pages for Active Directory administrative snap-ins to display in the property sheet for the object class. The shellPropertyPages contains the list for the Windows shell.

Each value is a string with the following format:

ordernumber,CLSID,optionaldata

In this example, the ordernumber is an unsigned number that represents the page's position on the sheet. When a property sheet is displayed, the values are sorted using a comparison of each value's ordernumber. If more than one value has the same ordernumber, those property page COM objects are loaded in the order they are read from Active Directory ; however, you should use a non-existing ordernumber (that is, one that has not been used by other values in the property). There is no prescribed starting position, and gaps are allowed in the ordernumber sequence.

The CLSID is the CLSID of the property page COM object in the string format produced by the StringFromGUID2 function in the COM library.

The optionaldata can be retrieved by the property page COM object using the IDataObject pointer passed to its IShellExtInit::Initialize method. The optionaldata is not required. The property page COM object calls IDataObject::GetData method with the clipboard format CF_DSPROPERTYPAGEINFO and gets an global memory handle (HGLOBAL) to a DSPROPERTYPAGEINFO structure containing the offset to the Unicode string containing optionaldata. A COM object can implement more than one property page. So, one possible use of the optionaldata is to name the pages to display. This gives you the flexibility of choosing to implement multiple COM objects (one for each page) or a single COM object to handle multiple pages.

The following string is an example value for the adminPropertyPages or shellPropertyPages properties:

1,{6dfe6485-a212-11d0-bcd5-00c04fd8d5b6}

Important For the Windows shell, display specifier information is retrieved at user logon, and cached for the user's session. For the administrative snap-ins, the display specifier information is retrieved when the snap-in is loaded, and is cached for the lifetime of the process. For the Windows shell, this means changes to display specifiers take effect after a user logs off and back on again. For the administrative snap-ins, changes take effect when the snap-in or console file is loaded.

Adding a Value to the adminPropertyPages or shellPropertyPages properties

When you add a value for your property page COM object to adminPropertyPages or shellPropertyPages, follow these rules:

  1. Make sure it hasn't already been added.
  2. Add a new value at the end of the property page ordering list. This means setting the ordernumber portion of the value to the next value after the highest existing ordernumber.
  3. To add the value, use the IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_APPEND so that the value will be added to the existing values (and, therefore, not overwrite the existing values). Make sure you call IADs::SetInfo to commit the change to the directory.

The following program adds a property page to the group class in the computer's default locale. Note how the AddPropertyPageToDisplaySpecifier function checks for the property page's CLSID in the existing values, gets the highest ordernumber, and adds the value for the property page using IADs::PutEx with the ADS_PROPERTY_APPEND control code.

#include <wchar.h>
#include <objbase.h>
#include <activeds.h>

HRESULT AddPropertyPageToDisplaySpecifier(
    LPOLESTR szClassName, //ldapDisplayName of class
    CLSID *pPropPageCLSID //CLSID of property page COM object
    );

HRESULT BindToDisplaySpecifiersContainerByLocale(LCID *locale,
                                        IADsContainer **ppDispSpecCont
                                          );

HRESULT GetDisplaySpecifier(IADsContainer *pContainer, LPOLESTR szDispSpec, IADs **ppObject);

void wmain( int argc, wchar_t *argv[ ])
{

wprintf(L"This program adds a sample property page to the display specifier for group class in the local computer's default locale.\n");

//Initialize COM.
CoInitialize(NULL);
HRESULT hr = S_OK;

//Class ID for the sample property page
LPOLESTR szCLSID = L"{D9FCE809-8A10-11d2-A7E7-00C04F79DC0F}";
LPOLESTR szClass = L"group";
CLSID clsid;
//Convert to GUID.
hr = CLSIDFromString(
    szCLSID,  //Pointer to the string representation of the CLSID.
    &clsid    //Pointer to the CLSID.
    );

hr = AddPropertyPageToDisplaySpecifier(
    szClass, //ldapDisplayName of class
    &clsid //CLSID of property page COM object
    );
if (S_OK == hr)
    wprintf(L"Property page registered successfully\n");
else if (S_FALSE == hr)
    wprintf(L"Property page was not added because it was already registered.\n");
else
    wprintf(L"Property page was not added. HR: %x.\n");

//Uninitialize COM.
CoUninitialize();
return;
}

//Adds a property page to Active Directory admin snap-ins.
HRESULT AddPropertyPageToDisplaySpecifier(
    LPOLESTR szClassName, //ldapDisplayName of class.
    CLSID *pPropPageCLSID //CLSID of property page COM object.
    )
{
HRESULT hr = E_FAIL;
IADsContainer *pContainer = NULL;
LPOLESTR szDispSpec = new OLECHAR[MAX_PATH];
IADs *pObject = NULL;
VARIANT var;
LPOLESTR szProperty = L"adminPropertyPages";
LCID locale = NULL;
//Get the display specifier container using default system locale.
//Note that when you are adding your property page COM object,
//you should specify the locale because the registration is
//locale specific. This means if you created a property page
//for German, you want to explicitly add it to the 407 container
//so that it will be used when a computer is running with locale
//set to German AND NOT whatever locale set on the
//computer where this program is running.
hr = BindToDisplaySpecifiersContainerByLocale(&locale,
                                        &pContainer
                                          );
//TODO: Need to handle fail case where dispspec object
//is not found and give option to create one.

if (SUCCEEDED(hr))
{
    //Bind to display specifier object for the specified class.
    //Build the display specifier name.
    wcscpy(szDispSpec, szClassName);
    wcscat(szDispSpec, L"-Display");
    hr = GetDisplaySpecifier(pContainer, szDispSpec, &pObject);
    if (SUCCEEDED(hr))
    {
        //Convert GUID to string.
        LPOLESTR szDSGUID = new WCHAR [39];
        ::StringFromGUID2(*pPropPageCLSID, szDSGUID, 39);

        //Get the adminPropertyPages property.
        hr = pObject->GetEx(szProperty,&var);
        if (SUCCEEDED(hr))
        {
          LONG lstart, lend;
          SAFEARRAY *sa = V_ARRAY( &var );
          VARIANT varItem;
          // Get the lower and upper bound.
           hr = SafeArrayGetLBound( sa, 1, &lstart );
          if (SUCCEEDED(hr))
          {
            hr = SafeArrayGetUBound( sa, 1, &lend );
          }
          if (SUCCEEDED(hr))
          {
            //Now iterate the values to check if the prop page's CLSID
            //is already registered.
            VariantInit(&varItem);
            BOOL bExists = FALSE;
            UINT uiLastItem = 0;
            UINT uiTemp = 0;
            INT iOffset = 0;
            LPOLESTR szMainStr = new OLECHAR[MAX_PATH];
            LPOLESTR szItem = new OLECHAR[MAX_PATH];
            LPOLESTR szStr = NULL;
            for ( long idx=lstart; idx < lend; idx++ )
            {
              hr = SafeArrayGetElement( sa, &idx, &varItem );
              if (SUCCEEDED(hr))
              {
                //Check if the specified CLSID is already registered.
                wcscpy(szMainStr,varItem.bstrVal);
                if (wcsstr( szMainStr,szDSGUID))
                    bExists = TRUE;
                //Get the index which is the number before the first comma.
                szStr = wcschr(szMainStr, ',');
                iOffset = (int)(szStr - szMainStr);
                wcsncpy( szItem, szMainStr, iOffset );
                szItem[iOffset]=0L;
                uiTemp = _wtoi(szItem);
                if (uiTemp > uiLastItem)
                    uiLastItem = uiTemp;
                VariantClear(&varItem);
              }
            }
            //If the CLSID is not registered, add it.
            if (!bExists)
            {
                //Build the value to add.
                LPOLESTR szValue = new OLECHAR[MAX_PATH];
                //Next index to add at end of list.
                uiLastItem++;
                _itow( uiLastItem, szValue, 10 );
                wcscat(szValue,L",");
                //Add the class ID for the property page.
                wcscat(szValue,szDSGUID);
//                  wprintf(L"Value to add: %s\n", szValue);
                VARIANT varAdd;
                //Only one value to add
                LPOLESTR pszAddStr[1];
                pszAddStr[0]=szValue;
                ADsBuildVarArrayStr(pszAddStr, 1, &varAdd);

                hr = pObject->PutEx( ADS_PROPERTY_APPEND, szProperty, varAdd );
                if (SUCCEEDED(hr))
                {
                    //Commit the change.
                    hr = pObject->SetInfo();
                }
            }
            else
                hr = S_FALSE;
          }
        }
        VariantClear(&var);
    }
    if (pObject)
    pObject->Release();
}

return hr;
}

//This function returns a pointer to the display specifier container
//for the specified locale.
//If locale is NULL, use the default system locale and then return the locale in the locale param.
HRESULT BindToDisplaySpecifiersContainerByLocale(LCID *locale,
                                        IADsContainer **ppDispSpecCont
                                          )
{
HRESULT hr = E_FAIL;

if ((!ppDispSpecCont)||(!locale))
    return E_POINTER;

//If no locale is specified, use the default system locale.
if (!(*locale))
{
    *locale = GetSystemDefaultLCID();
    if (!(*locale))
    return E_FAIL;
}

//Make sure that it's a valid locale.
if (!IsValidLocale(*locale, LCID_SUPPORTED))
    return E_INVALIDARG;

LPOLESTR szPath = new OLECHAR[MAX_PATH*2];
IADs *pObj = NULL;
VARIANT var;

hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication.
                     IID_IADs,
                     (void**)&pObj);

if (SUCCEEDED(hr))
{
    //Get the DN to the config container.
    hr = pObj->Get(L"configurationNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build the string to bind to the DisplaySpecifiers container.
        swprintf(szPath,L"LDAP://cn=%x,cn=DisplaySpecifiers,%s", *locale, var.bstrVal);
        //Bind to the DisplaySpecifiers container.
        *ppDispSpecCont = NULL;
        hr = ADsOpenObject(szPath,
            NULL,
            NULL,
            ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
            IID_IADsContainer,
            (void**)ppDispSpecCont);

        if(FAILED(hr))
        {
            if (!(*ppDispSpecCont))
            {
                (*ppDispSpecCont)->Release();
                (*ppDispSpecCont) = NULL;
            }
        }
    }
}
//Clean up
VariantClear(&var);
if (pObj)
    pObj->Release();

return hr;
}

HRESULT GetDisplaySpecifier(IADsContainer *pContainer, LPOLESTR szDispSpec, IADs **ppObject)
{
HRESULT hr = E_FAIL;
LPOLESTR szDSPath = new OLECHAR [MAX_PATH];
IDispatch *pDisp = NULL;

//Build relative path to the display specifier object.
wcscpy(szDSPath, L"CN=");
wcscat(szDSPath, szDispSpec);
if (!pContainer)
{
    hr = E_POINTER;
    return hr;
}
//TODO check the other pointers.
//Initialize the output pointer.
(*ppObject) = NULL;

//Use child object binding with IADsContainer::GetObject.
hr = pContainer->GetObject(L"displaySpecifier",
                      szDSPath,
                      &pDisp);
if (SUCCEEDED(hr))
{
    hr = pDisp->QueryInterface(IID_IADs, (void**)ppObject);
    if (FAILED(hr))
    {
        //Clean up
        if (*ppObject)
        (*ppObject)->Release();
    }
}

if (pDisp)
    pDisp->Release();

return hr;

}


Context Menus for Use with Display Specifiers

You can add context menus to the existing context menus for a particular object class within the Windows shell or Active Directory administrative snap-ins. Context menu items may be either COM objects, which are activated using the standard COM instance creation methods, or an application that is invoked using the standard ShellExecute function.

For example, you can implement a context menu COM object that adds menu items to the context menu of the contact class. When a user views a contact object, your context menu items are displayed along with other context menu items in the contact context menu.

To extend context menus with context menu items from a COM object

  1. Write the property page COM object. The property page COM object must implement IShellExtInit and IContextMenu.
  2. Install the context menu COM object on the computers where you want the extension context menu items to be used. It is recommended that you create a Microsoft Installer package for your context menu COM object's DLL and deploy the package appropriately using the group policy. See Using MSI and Windows 2000 Application Deployment to Distribute UI Additions.
  3. Register the context menu extension for a particular object class so that the context menu is added to the context menus of objects of that class.

    For the Windows shell or Active Directory administrative snap-ins to recognize your context menu, you must add a value for your context menu COM object in the adminContextMenu and/or shellContextMenu property of the display specifier for the class whose context menu you want to extend. Note that you can register the same context menu COM object for more than one class.

    Note that registration is per locale. If the context menu COM object is specific to a certain locale, it should be registered in a displaySpecifier in the locale container that it applies to. If it applies to all locales, it should be added to the appropriate displaySpecifier (that is, the one that represents the class that should get the context menu extension) in all locale containers.

To extend context menus with context menu items that launch an application

  1. Install the application where you want the extension context menu items to be used.
  2. Register the context menu extension for a particular object class so that the context menu is added to the context menus of objects of that class. Note that value added for the extension takes a slightly different format.


Implementing the Context Menu COM Object

To extend context menus with custom menu items for directory objects, a context menu COM object must implement two shell interfaces: IShellExtInit and IContextMenu.

A context menu COM object is instantiated when the user opens the context menu for an object of a class for which the context menu extension has been registered. If the COM object was registered in the class's adminContextMenu property, the COM object's context menu items appear in the context menu for objects of that class in Active Directory administrative snap-ins. If it was registered in shellContextMenu, the context menu items appear in the Windows shell.

After the context menu COM object is instantiated, the IShellExtInit::Initialize method is called. The IShellExtInit interface has only one method and it must be implemented. This is the context menu COM object's only opportunity to get the IDataObject pointer that enables the object to get information about the selected directory objects. Follow the same rules and recommendations as the implementation for property page COM objects. See Implementing the Property Page COM Object.

After IShellExtInit::Initialize returns, the IContextMenu::QueryContextMenu method is called. The IContextMenu interface has three methods that must be implemented:

IContextMenu::QueryContextMenu
Called just before the context menu is displayed. This is the context menu COM object's opportunity to add menu items using the InsertMenu function.
IContextMenu::GetCommandString
Called when a context menu item is selected. This is the context menu COM object's opportunity to display status bar text. Make sure that you cast the pszName parameter to a Unicode string when specifying the text to display.
IContextMenu::InvokeCommand
Called when a context menu item is chosen. This is the context menu COM object's opportunity to perform the action represented by the menu item.


Example Code for Implementation of the Context Menu COM Object

The following code fragment contains an implementation of the IShellExtInit IContextMenu methods and the command handler methods for each menu command added by the IContextMenu::QueryContextMenu method. The context menu items display message boxes with various information (such as the contents of the two supported clipboard formats). For the full implementation of this sample, see the following MyContextMenu sample.

STDMETHODIMP CMyDsContextMenu::QueryContextMenu(HMENU hMenu,
                                         UINT indexMenu,
                                         UINT idCmdFirst,
                                         UINT idCmdLast,
                                         UINT uFlags)
{
    UINT idCmd = idCmdFirst;
    WCHAR szMenuText1[64];
    WCHAR szMenuText2[64];
    WCHAR szMenuText3[64];
    WCHAR szMenuText4[64];
    WCHAR *szMenuGetSupportedClipboardFormats = L"Get Supported Clipboard Formats";
    WCHAR *szMenuGetDsDisplaySpecOptions = L"Get Data Using DsDisplaySpecOptions Clipboard Format";
    WCHAR *szMenuGetDsObjectNames = L"Get Data Using DsObjectNames Clipboard Format";
    BOOL bAppendItems=TRUE;

    //Check the flags
    //Normal
    //Note that the administrative snap-ins will
    //use only the CMF_NORMAL state.
    //So CMF_EXPLORE will never be set for admin snap-ins.
    if ((uFlags & 0x000F) == CMF_NORMAL)  //Check == here, since CMF_NORMAL=0
    {
        wcscpy(szMenuText1, L"Do DoMenuCmd&1 (Normal)");
        wcscpy(szMenuText2, L"Do DoMenuCmd&2 (Normal)");
        wcscpy(szMenuText3, L"Do DoMenuCmd&3 (Normal)");
        wcscpy(szMenuText4, L"Do DoMenuCmd&4 (Normal)");
    }
    //Note that if the context menu is a shell extension,
    //the Windows shell may be displaying the object in Explorer view.
    //This can be handled differently based on what you need/want.
    //Explorer window
    else if (uFlags & CMF_EXPLORE)
    {
        wcscpy(szMenuText1, L"Do DoMenuCmd&1 (Explorer)");
        wcscpy(szMenuText2, L"Do DoMenuCmd&2 (Explorer)");
        wcscpy(szMenuText3, L"Do DoMenuCmd&3 (Explorer)");
        wcscpy(szMenuText4, L"Do DoMenuCmd&4 (Explorer)");
    }
    else if (uFlags & CMF_DEFAULTONLY)
    {
        bAppendItems = FALSE;
    }
    else
    {
        bAppendItems = FALSE;
    }

    //Insert the menu items.
    if (bAppendItems)
    {
        InsertMenuW(hMenu, indexMenu++, MF_SEPARATOR|MF_BYPOSITION, 0, NULL);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuText1);

        InsertMenuW(hMenu, indexMenu++, MF_SEPARATOR|MF_BYPOSITION, 0, NULL);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuText2);

        InsertMenuW(hMenu, indexMenu++, MF_SEPARATOR|MF_BYPOSITION, 0, NULL);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuText3);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuText4);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuGetSupportedClipboardFormats);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuGetDsDisplaySpecOptions);

        InsertMenuW(hMenu,
                   indexMenu++,
                   MF_STRING|MF_BYPOSITION,
                   idCmd++,
                   szMenuGetDsObjectNames);

        return ResultFromShort(idCmd-idCmdFirst); //Must return number of menu
        //items we added.
    }
    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi)
{

    HRESULT hr = E_INVALIDARG;

    if (!HIWORD(lpcmi->lpVerb))
    {
        UINT idCmd = LOWORD(lpcmi->lpVerb);

        switch (idCmd)
        {
            case 0:
                hr = DoMenuCmd1(lpcmi->hwnd,
                                lpcmi->lpDirectory,
                                lpcmi->lpVerb,
                                lpcmi->lpParameters,
                                lpcmi->nShow);
                break;

            case 1:
                hr = DoMenuCmd2(lpcmi->hwnd,
                                lpcmi->lpDirectory,
                                lpcmi->lpVerb,
                                lpcmi->lpParameters,
                                lpcmi->nShow);
                break;

            case 2:
                hr = DoMenuCmd3(lpcmi->hwnd,
                                lpcmi->lpDirectory,
                                lpcmi->lpVerb,
                                lpcmi->lpParameters,
                                lpcmi->nShow);
                break;

            case 3:
                hr = DoMenuCmd4(lpcmi->hwnd,
                                lpcmi->lpDirectory,
                                lpcmi->lpVerb,
                                lpcmi->lpParameters,
                                lpcmi->nShow);
            case 4:
                hr = GetSupportedClipboardFormats(lpcmi->hwnd,m_pDataObj);
                break;

            case 5:
                hr = GetDsDisplaySpecOptions(lpcmi->hwnd,m_pDataObj);
                break;

            case 6:
                hr = GetDsObjectNames(lpcmi->hwnd,m_pDataObj);
                break;
        }
    }
/*
    else if (HIWORD(lpcmi->lpVerb))
    {
    //Handle this for returning language independent command names.

    }
*/
    return hr;
}

//
//  FUNCTION: CMyDsContextMenu::GetCommandString
//
//  Called by the shell after the user has selected a
//  menu item that was added in QueryContextMenu().
//

STDMETHODIMP CMyDsContextMenu::GetCommandString(UINT idCmd,
                                         UINT uFlags,
                                         UINT FAR *reserved,
                                         LPSTR pszName,
                                         UINT cchMax)
{
if ((uFlags & GCS_HELPTEXT) != 0)
{
    switch (idCmd)
    {
    //Need to cast pszName to a Unicode string.
    //TODO check pszName length against cchMax
        case 0:
            wcscpy((LPOLESTR)pszName, L"New menu item number 1");
            break;

        case 1:
            wcscpy((LPOLESTR)pszName, L"New menu item number 2");
            break;

        case 2:
            wcscpy((LPOLESTR)pszName, L"New menu item number 3");
            break;

        case 3:
            wcscpy((LPOLESTR)pszName, L"New menu item number 4");
            break;
        case 4:
            wcscpy((LPOLESTR)pszName, L"Get Supported Clipboard Formats");
        case 5:
            wcscpy((LPOLESTR)pszName, L"Get Data Using DsDisplaySpecOptions Clipboard Format");
        case 6:
            wcscpy((LPOLESTR)pszName, L"Get Data Using DsObjectNames Clipboard Format");
    }
    return S_OK;
}
/*
else if (uFlags == GCS_VERB)
{
    //Handle this flag for returning language independent command names

    }
*/
return E_FAIL;
}

//Command Handlers

STDMETHODIMP CMyDsContextMenu::DoMenuCmd1(HWND hParent,
                                   LPCSTR pszWorkingDir,
                                   LPCSTR pszCmd,
                                   LPCSTR pszParam,
                                   int iShowCmd)
{

    MessageBoxW(hParent, L"Menu item 1!", L"Shell Extension Sample", MB_OK);

    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::DoMenuCmd2(HWND hParent,
                                   LPCSTR pszWorkingDir,
                                   LPCSTR pszCmd,
                                   LPCSTR pszParam,
                                   int iShowCmd)
{

    MessageBoxW(hParent, L"Menu item 2!", L"Shell Extension Sample", MB_OK);

    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::DoMenuCmd3(HWND hParent,
                                   LPCSTR pszWorkingDir,
                                   LPCSTR pszCmd,
                                   LPCSTR pszParam,
                                   int iShowCmd)
{

    MessageBoxW(hParent, L"Menu item 3!", L"Shell Extension Sample", MB_OK);

    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::DoMenuCmd4(HWND hParent,
                                   LPCSTR pszWorkingDir,
                                   LPCSTR pszCmd,
                                   LPCSTR pszParam,
                                   int iShowCmd)
{

    MessageBoxW(hParent, L"Menu item 4!", L"Shell Extension Sample", MB_OK);

    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::Initialize(LPCITEMIDLIST pIDFolder,
                                   LPDATAOBJECT pDataObj,
                                   HKEY hRegKey)
{

    // Initialize can be called more than once

    if (m_pDataObj)
        m_pDataObj->Release();

    // Keep a pointer to the IDataObject

    if (pDataObj)
    {
        m_pDataObj = pDataObj;
        pDataObj->AddRef();
    }

    //Register the clipboard formats to use for getting info about
    //the selected DS object.
    m_cfDsDispSpecOptions = RegisterClipboardFormat(CFSTR_DS_DISPLAY_SPEC_OPTIONS);
    m_cfDsObjectNames = RegisterClipboardFormat(CFSTR_DSOBJECTNAMES);

    return NOERROR;
}

STDMETHODIMP CMyDsContextMenu::GetSupportedClipboardFormats(HWND hParent, IDataObject *pDO)
{
#define FORMATETC_MAX 20
    LPOLESTR szText = new OLECHAR[MAX_PATH*4];
    OLECHAR szCFName[MAX_PATH];
    HRESULT hr = E_FAIL;
    LPENUMFORMATETC lpEnumFmtEtc = NULL;
    FORMATETC rgfmtetc[FORMATETC_MAX];
    int iReturn = 0;
    ULONG j;
    ULONG cFetched = 0;

    hr = pDO->EnumFormatEtc(
        DATADIR_GET,
        (LPENUMFORMATETC FAR*)&lpEnumFmtEtc
        );
    if (FAILED(hr))
    {
    //Not implemented in Users and Computers snap-in.
    if (E_NOTIMPL==hr)
        swprintf(szText,L"EnumFormatEtc method is not implemented in the Users and Computers snap-in.");
    else
        swprintf(szText,L"EnumFormatEtc method failed with hr: %x",hr);

    MessageBoxW(hParent,szText,L"Clipboard formats",MB_OK);
        return hr;    // unable to get format enumerator
    }
    //Is implemented in the Windows shell.
    //Enumerate the formats offered by the source.
    // Loop over all formats offered by the source.
    memset(rgfmtetc,0,sizeof(rgfmtetc[FORMATETC_MAX]) );
    hr = lpEnumFmtEtc->Next(FORMATETC_MAX,
                    rgfmtetc,
                    &cFetched);
    if ( SUCCEEDED(hr) || (cFetched > 0 && cFetched <= FORMATETC_MAX) )
    {
    wcscpy(szText,L"Supported clipboard formats:\n");
        for (j = 0; j < cFetched; j++)
        {
            iReturn = GetClipboardFormatNameW(
                rgfmtetc[j].cfFormat, //Clipboard format to retrieve.
                szCFName,  //Address of buffer for name.
                sizeof(szCFName)         //Length of name string in characters.
                );
        if (iReturn)
        {
            wcscat(szText,szCFName );
            wcscat(szText,L"\n");
        }
        else
        {
            swprintf(szCFName,L"Unknown clipboard format %d\n",rgfmtetc[j].cfFormat);
            wcscat(szText,szCFName );
        }
    }
    MessageBoxW(hParent,szText,L"Clipboard formats",MB_OK);
}
    else
    {
    swprintf(szText,L"Next method failed with hr: %x",hr);
    MessageBoxW(hParent,szText,L"Clipboard formats",MB_OK);
    }

    //Clean up
    if (lpEnumFmtEtc)
        lpEnumFmtEtc->Release();
    return hr;
}

STDMETHODIMP CMyDsContextMenu::GetDsDisplaySpecOptions(HWND hParent,
                                        IDataObject *pDO)
{

LPOLESTR szText = new OLECHAR[MAX_PATH*4];
HRESULT hr = S_OK;
PDSDISPLAYSPECOPTIONS pDsSpecOptions = NULL;
LPOLESTR szPrefix = NULL;

//Need to get CFSTR_DS_DISPLAY_SPEC_OPTIONS as HGLOBAL.
STGMEDIUM stgmedium = {
    TYMED_HGLOBAL,
    NULL,
    NULL
};

//Initialize formatetc for CFSTR_DS_DISPLAY_SPEC_OPTIONS format.
FORMATETC formatetc = {
    m_cfDsDispSpecOptions,
    NULL,
    DVASPECT_CONTENT,
    -1,
    TYMED_HGLOBAL
};

//Get the global memory block containing the display option info for the selected objects.
hr = pDO->GetData(&formatetc, &stgmedium);
if (FAILED(hr))
{
    //Not implemented.
    if (E_NOTIMPL==hr)
        swprintf(szText,L"GetData method is not implemented.");
    else
        swprintf(szText,L"GetData method failed with hr: %x",hr);

    MessageBoxW(hParent,szText,L"GetData using DsDisplaySpecOptions",MB_OK);
    return hr;    //Unable to get data.
}
// Assign pointer to DSDISPLAYSPECOPTIONS structure.
pDsSpecOptions = (PDSDISPLAYSPECOPTIONS) GlobalLock(stgmedium.hGlobal);
if (pDsSpecOptions)
{
    //Use the ByteOffset macro that was defined
    //to move to the appropriate offset based on offsetAttributePrefix.
    szPrefix = (LPOLESTR)ByteOffset(pDsSpecOptions,
                                   pDsSpecOptions->offsetAttribPrefix);

    if (0==wcscmp(DS_PROP_SHELL_PREFIX, szPrefix))
        swprintf(szText,L"Context menu was called from the Windows shell.\nThe offsetAttribPrefix member is %s.", szPrefix);
    else if (0==wcscmp(DS_PROP_ADMIN_PREFIX, szPrefix))
        swprintf(szText,L"Context menu was called from an Active Directory Administrative Snap-in.\nThe offsetAttribPrefix member is %s.", szPrefix);
    else
        swprintf(szText,L"Could not determine prefix. The offsetAttribPrefix member is %s.", szPrefix);
    MessageBoxW(hParent,szText,L"GetData using DsDisplaySpecOptions",MB_OK);
}

//Clean up
GlobalUnlock(stgmedium.hGlobal);
ReleaseStgMedium(&stgmedium);

return hr;

}


Registering the Context Menu COM Object in a Display Specifier

Even after you've written and installed your context menu COM object, the Windows shell and Active Directory administrative snap-ins cannot recognize it yet. For the Windows shell or Active Directory administrative snap-ins to recognize your context menu, you must add a value for your context menu COM object in the adminContextMenu and/or shellContextMenu property of the display specifier for the class whose context menu you want to extend. Note that you can register the same context menu COM object for more than one class.

If the COM object was registered in the adminContextMenu property of the class's display specifier, the context menu appears when the user opens the context menu for objects of that class in Active Directory administrative snap-ins. If it was registered in shellContextMenu, the context menu appears in the Windows shell. If you want it to appear in both the shell and administrative snap-ins, you need to register the COM object in both properties.

The adminContextMenu and shellContextMenu properties are multi-valued. The adminContextMenu contains the list of context menus for Active Directory administrative snap-ins to add to the context menu for the object class. The shellContextMenu contains the list for the Windows shell.

Each value is a string with the following format:

For a context menu COM object, the value is a string with the following format:

ordernumber,CLSID

In this example, the ordernumber is a signed number that represents the menu item's position on the context menu. When a context menu is first displayed, the values are sorted using a signed comparison of each value's ordernumber. If more than one value has the same ordernumber, those context menu COM objects are loaded in the order they are read from the directory; however, you should use a non-existing ordernumber (that is, one that has not been used by other values in the property). There is no prescribed starting position, and gaps are allowed in the ordernumber sequence.

The CLSID is the CLSID of the context menu's COM object in the string format produced by the StringFromGUID2 function in the COM library. Note that the COM object must implement the IContextMenu and IShellExtInit interfaces.

Example:

1,{08eb4fa6-6ffd-11d1-b0e0-00c04fd8dca6}

Adding a Value to the adminContextMenu or shellContextMenu properties

When you add a value for your property page COM object to adminContextMenu or shellContextMenu, follow these rules:

  1. Make sure it hasn't already been added.
  2. Add a new value at the end of the context menu ordering list. This means setting the ordernumber portion of the value to the next value after the highest existing ordernumber.
  3. To add the value, use the IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_APPEND so that the value will be added to the existing values (and, therefore, not overwrite the existing values). Make sure you call IADs::SetInfo to commit the change to the directory.

See the sample code for adding values for property pages.


Registering a Context Menu Item that Starts an Application in a Display Specifier

If the context menu item starts a program, the value is a string with the following format:

ordernumber,menuitemtext,command

In this example, the ordernumber is a signed number that represents the menu item's position on the context menu. When a context menu is first displayed, the values are sorted using a signed comparison of each value's ordernumber. If more than one value has the same ordernumber, those context menu items are loaded in the order they are read from the directory; however, you should use a non-existing ordernumber (that is, one that has not been used by other values in the property). There is no prescribed starting position and gaps are allowed in the ordernumber sequence.

The menuitemtext is the text used to display this context menu item on the context menu. The menuitemtext cannot contain commas. The menuitemtext can contain an ampersand (&) to set the shortcut key for the menu item. For example, the following value makes d the shortcut key:

1,&Do this,do.exe

The command is the program that will be executed by the snap-in. Either the full path must be specified or the application must be in the computer's path environment variable. The program is invoked using the ShellExecute function. The command cannot contain additional parameters (for example, Notepad myfile.txt). Since ShellExecute is used, documents can be specified as the command (for example the command d:\myfile.doc will open d:\myfile.doc based on the program associated to the .doc extension). Paths and application names with spaces are allowed. Note that the selected object's distinguished name and class are passed as the first and second arguments, respectively, of the program invoked by command.

In the Windows shell, multi-selection is supported. The command is invoked for each selected object. In Active Directory administrative snap-ins, multi-selection is not supported, that is, the command is not invoked at all. Only single selection is supported.

Important For the Windows shell, display specifier information is retrieved at user logon and cached for the user's session. For the administrative snap-ins, the display specifier information is retrieved when the snap-in is loaded and is cached for the lifetime of the process. For the Windows shell, this means changes to display specifiers take effect after a user logs off and back on again. For the administrative snap-ins, changes take effect when the snap-in or console file is reloaded (that is, if you start a new instance of the console file or new MMC.EXE instance and add the snap-in, the latest display specifier information will be retrieved).


Class and Attribute Display Names

Each object class may also have a class display name and each attribute of that class may have an attribute display name.

The class display name is stored in the classDisplayName attribute as a single valued Unicode string.

The attribute display names are stored in the attributeDisplayName attribute as multi-valued Unicode strings, with each element consisting of a comma delimited name pair:

attrName,DisplayText

In this example, the attrName is the attribute ldapDisplayName and the DisplayText is the text to display as the label (name) of that attribute in the user interface.

Guidelines for Class and Attribute Display Names

Since many vendors may be extending classes with new attributes or creating entirely new classes, it is important that the class and attribute display names are unambiguous and do not result in conflicts.

Each vendor should prefix the class display name with a unique friendly identifier based on the vendor's name. For example if the Arcadia Bay company creates a new class derived from the user class, then they could uniquely have a class display name "Arcadia Bay User"

If a vendor extends an existing class with new attributes, then they should again uniquely identify the attribute display name so that there are no conflicts with other attribute display names. Again, prefixing the attribute display name with unique friendly identifier based on the vendor's name is good practice. For example, if the Arcadia Bay company extends the user class with a new HR attribute, they could uniquely display the attribute as "Arcadia Bay HR Information"

Additionally, from a localization perspective, each vendor should localize the class and attribute display names into each language supported by Windows 2000.

Adding a Value to the attributeDisplayName Property

When you add a value for an attribute display name to attributeDisplayName, follow these rules:

  1. Check if a mapping for the attribute already exists. If you want to replace an existing attribute value, delete it first using IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_DELETE and the vProp parameter set to the value you want removed.

    Do not use ADS_PROPERTY_CLEAR or ADS_PROPERTY_UPDATE.

  2. Create the string representing the attribute display name. See the format above.
  3. To add the value, use the IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_APPEND so that the value will be added to the existing values (and, therefore, not overwrite the existing values). Make sure you call IADs::SetInfo to commit the change to the directory.


Class Icons

The iconic images used to represent a class object can be read from the Display Specifier. Moreover, each class can store multiple icon states. For example, a folder class can have bitmaps for the open, closed, and disabled states. The current implementation allows up to sixteen different icon states per class.

The attribute is named iconPath and can be specified in one of two ways.

state,ICOfilename

or

state,DLLname,resourceID

In this example, the state is an integer with a value between 0 (zero) and 15. The value zero is defined to be the default or "closed" state of the icon. The value 1 is defined to be the "open" state of the icon. The value 2 is the disabled state. The other values are application-defined.

The ICOfilename or DLLname must be the name of a file in the local computer's file search path.

The resourceID is a zero-based index into the DLL's resource fork list of icons.

Adding a Value to the iconPath Property

When you add a value for an icon to iconPath, follow these rules:

  1. Check if an icon for the state intended for the new icon exists. If you want to replace an existing icon value, delete it first using IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_DELETE and the vProp parameter set to the value you want removed.

    Do not use ADS_PROPERTY_CLEAR or ADS_PROPERTY_UPDATE.

  2. Set the state portion of the value to the icon state for the new icon that you want to add.
  3. To add the value, use the IADs::PutEx method with the lnControlCode parameter set to ADS_PROPERTY_APPEND so that the value will be added to the existing values (and, therefore, not overwrite the existing values). Make sure you call IADs::SetInfo to commit the change to the directory.


Viewing Containers as Leaf Nodes

Potentially, any Active Directory object can be a container of other objects. This can clutter the UI, so it is possible to declare that a specific class be displayed as a leaf element by default. The treatAsLeaf attribute holds a boolean value that, if True, indicates that the objects of the class should only be treated as leaf elements.


Object Creation Wizards

Note that the ability to implement extensions to existing creation wizards and replacement primary extensions will be available post-Beta 3. Existing creation wizard can be invoked from other applications in Beta 3.

In Active Directory, administrative snap-ins, the user can create new objects in the directory by opening the context menu for the container where the new object will be created, choosing New, and then choosing the class of object to create. Creating new instances of an object invokes the object creation wizard. Each class of object may specify the use of a specific creation wizard, or it may use a generic creation wizard. For well known classes (for example, user and organizationalUnit), the Active Directory Users and Computers snap-in provides a standard set of creation wizards.

Note that object creation wizards can be invoked from other applications. For more information, see Invoking Creation Wizards from Your Application.

There are two methods for extending creation wizards:

  • Replace an existing wizard (or provide one if one does not exist for the class). The existing wizard is replaced through the use of a primary extension. A primary extension provides the first set of pages and is hosted in the same way as native pages. It also supports the extensibility mechanism so that other creation wizard extensions can be invoked.
  • Extend an existing wizard. Existing wizards can be extended with creation wizard extensions, which add additional pages following those of the native or primary extension.

In both cases, the extension UI has to be implemented as a COM object and it must support the IDsAdminWizExt interface.

If a creation wizard is not registered for an object class, Active Directory administrative snap-ins provide a generic creation wizard. The generic creation wizard is built at run time from the list of mandatory properties for the class of object that is being created. For each mandatory property, a page is added to the UI. The generic creation wizard is not extensible. If extensibility is required, it must be replaced with a primary extension.

To replace the existing creation wizard (or the generic creation wizard if no creation wizard is registered) for an object class

  1. Write the primary extension COM object. The COM object must implement IDsAdminWizExt and provide pages .
  2. Install the context menu COM object on the computers where you want the extension context menu items to be used. It is recommended that you create a Microsoft Installer package for your context menu COM object's DLL and deploy the package appropriately using the group policy. See Using MSI and Windows 2000 Application Deployment to Distribute UI Additions.
  3. Register the creation wizard as the primary extension for a particular object class.

    Add the value for your creation wizard COM object in the creationWizard property of the display specifier for the class for which your creation wizard will be the primary extension. Note that you can register the same creation wizard COM object for more than one class.

    Note that registration is per locale. If the creation wizard COM object is specific to a certain locale, it should be registered in a displaySpecifier in the locale container that it applies to. If it applies to all locales, it should be added to the appropriate displaySpecifier (that is, the one that represents the class that should get the creation wizard) in all locale containers.

To extend context menus with context menu items that launch an application

  1. Write the primary extension COM object. The COM object must implement IDsAdminWizExt and provide pages .
  2. Install the creation wizard COM object on the computers where you want the creation wizard to be used. It is recommended that you create a Microsoft Installer package for your creation wizard COM object's DLL and deploy the package appropriately using the group policy. See Using MSI and Windows 2000 Application Deployment to Distribute UI Additions.
  3. Register the creation wizard as an extension for a particular object class.

    Add a value for your creation wizard COM object to the existing values in the creationWizardExt property of the display specifier for the class whose creation wizard you want to extend. Note that you can register the same creation wizard COM object for more than one class.

    Note that registration is per locale. If the creation wizard COM object is specific to a certain locale, it should be registered in a displaySpecifier in the locale container that it applies to. If it applies to all locales, it should be added to the appropriate displaySpecifier (that is, the one that represents the class that should get the creation wizard) in all locale containers.


Invoking Creation Wizards from Your Application

Using the IDsAdminCreateObj COM object, an application can use the creation wizard for a specified class to create a new object of that class. For example, an application can use IDsAdminCreateObj to invoke the creation wizard for creating a new user.

The following function starts the user creation wizard so that the user can create a new user in the container (specified by an IADsContainer pointer). The function returns an IADs pointer to the new user object.

//Need to include dsadmin.h
//Need to link dsuiext.lib

HRESULT StartCreateUserWizard(HWND hWnd, //Handle to window that should own the wizard.
                        IADsContainer *pContainer, //Container in which to create the new object
                        IADs **ppNewObject //Return a pointer the new object
                        )
{
if ((!pContainer)||(!ppNewObject))
    return E_POINTER;
CHAR *szText = new CHAR[MAX_PATH*4];
VARIANT varname;
BSTR bstr;
HRESULT hr = E_FAIL;
//For creating a new user, set class to user.
LPOLESTR szClass = L"user";

//Create the creation handler for creation wizards
IDsAdminCreateObj* pCreateObj = NULL;
hr = ::CoCreateInstance(CLSID_DsAdminCreateObj,
                        NULL, CLSCTX_INPROC_SERVER,
                        IID_IDsAdminCreateObj,
                        (void**)&pCreateObj);
if (SUCCEEDED(hr))
{
    // Initialize handler
    hr = pCreateObj->Initialize(pContainer, szClass);
    if (SUCCEEDED(hr))
    {
        //Invoke the creation wizard.
        hr = pCreateObj->CreateModal(hWnd, ppNewObject);
        //S_FALSE means user clicked Cancel
        if (hr == S_FALSE)
        {
        MessageBox(hWnd,"User cancelled the wizard\nThe new object was not created.","Start Create User Wizard",MB_OK);
        //Set *ppNewObject to NULL
        *ppNewObject = NULL;
        hr = E_FAIL;
        }
        //S_OK means user clicked OK
        else if (hr==S_OK)
        {
        //Display the new user object's name and ADsPath
        hr = (*ppNewObject)->Get(L"name",&varname);
        hr = (*ppNewObject)->get_ADsPath(&bstr);
        if (SUCCEEDED(hr))
            {
            wsprintf(szText,"The new user %ws was successfully created. \nNew object ADsPath: %ws",varname.bstrVal,bstr);
            MessageBox(NULL,szText,"Start Create User Wizard",MB_OK);
            VariantClear(&varname);
            SysFreeString(bstr);
            }
        else
            {
            wsprintf(szText,"The new object was successfully created. But properties could not be read from the new object.");
            MessageBox(hWnd,szText,"Start Create User Wizard",MB_OK);
            //Return S_FALSE to tell caller that the property read failed.
            hr = S_FALSE;
            }
        }
        else
        {
        wsprintf(szText,"Creation Wizard could not be started. hr: %x",hr);
        MessageBox(hWnd,szText,"Start Create User Wizard",MB_OK);
        }
    }
}
if (pCreateObj)
    pCreateObj->Release();

return hr;
}


Using MSI and Windows 2000 Application Deployment to Distribute UI Additions

To distribute the COM objects, applications, and/or files used by the extensions you've added to a display specifier (property page, context menu, and creation wizard COM objects, ICO files for class icons, scripts or applications started by context menu extensions, and so on), you can create a Microsoft Installer package and use group policy to deploy it to users or computers.


Using Standard Dialog Boxes for Handling Active Directory Objects

Microsoft Windows provides a Common Dialog Box library for common operations (such as File Open, File Browse, and so on). The Windows 2000 provides some dialog boxes that you can use for common UI operations in Active Directory.

The Windows 2000 operating system provides COM objects that implement dialog boxes for handling directory objects:

  • Directory Object Picker (for specific types)
  • Domain Browser
  • Container Browser
  • Query Forms

In addition, Active Directory creation wizards can also be invoked from an application. For more information, see Invoking Creation Wizards from Your Application.


Directory Object Picker

Using the IDsObjectPicker COM object, an application can display a dialog box that enables a user to select one or more objects from the directory and get back information about the selected objects. Note that the object picker supports only the selection of user, contact, group, and computer objects. Note that well-known security principals (such as Everyone, Anonymous, and so on) can also be included as selectable object in the object picker.

To display an object picker dialog box, call the CoCreateInstance or CoCreateInstanceEx function to create an instance of the system's IDsObjectPicker implementation. Then call the IDsObjectPicker::Initialize method to initialize the dialog box and the IDsObjectPicker::InvokeDialog method to display it.

When you initialize an object picker dialog box, you specify a set of scope types and filters.

  • The specified scope types determine the locations, for example domains or computers, from which the user can select objects.
  • The filters determine the types of objects the user can select from a given scope type.

The Look in drop-down list contains the scope locations that correspond to the specified scope types. When the user selects a scope from the list, the dialog box applies the filters for that scope type to display a list of objects.

By default, the user can select a single object. To enable multiple selections, set the DSOP_FLAG_MULTISELECT flag in the flOptions member of the DSOP_INIT_INFO structure when you initialize the dialog box.

For more information about the IDsObjectPicker interface and its related structures, see the Platform SDK documentation.

The following functions display the object picker dialog box so that the user can select computer objects. The SelectComputersAndDo function is the main function and calls the InitObjectPicker and ProcessSelectedObjects functions. The InitObjectPicker function initializes the IDsObjectPicker object to search for computers in the current domain. The ProcessSelectedObjects function displays the name, objectClass, and ADsPath of the selected objects in message boxes.

//Need to include objsel.h
UINT g_cfDsObjectPicker; //for clipboard format.

HRESULT SelectComputersAndDo(HWND hWnd) //Handle to window that should own the object picker.
{
HRESULT hr = E_FAIL;
IDsObjectPicker *pDsObjectPicker = NULL;
IDataObject *pdo = NULL;
// Create an instance of the object picker.
hr = CoCreateInstance(CLSID_DsObjectPicker,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IDsObjectPicker,
    (void **) &pDsObjectPicker);
if (SUCCEEDED(hr))
{
    // Initialize the object picker instance.
    hr = InitObjectPicker(pDsObjectPicker);
    // Invoke the modal dialog.
    if (SUCCEEDED(hr))
    {
    hr = pDsObjectPicker->InvokeDialog(hWnd, &pdo);
    if (hr == S_OK) //S_OK means User clicked OK
    {
        //Process the selected objects and display each in a message box.
    ProcessSelectedObjects(pdo);
    if (pdo)
        pdo->Release();
    }
    else if (hr == S_FALSE) //S_FALSE means User clicked Cancel
    MessageBoxA(hWnd,"User canceled object picker dialog\n","Object Picker dialog",MB_OK);
    else
        MessageBoxA(hWnd,"Object picker dialog failed.","Object Picker dialog",MB_OK);
    }
}
if (pDsObjectPicker)
    pDsObjectPicker->Release();
return hr;
}
HRESULT InitObjectPicker(
    IDsObjectPicker *pDsObjectPicker) {

g_cfDsObjectPicker =    RegisterClipboardFormat(CFSTR_DSOP_DS_SELECTION_LIST);

static const int     SCOPE_INIT_COUNT = 1;
DSOP_SCOPE_INIT_INFO aScopeInit[SCOPE_INIT_COUNT];
DSOP_INIT_INFO  InitInfo;
// Initialize the DSOP_SCOPE_INIT_INFO array.

ZeroMemory(aScopeInit,
    sizeof(DSOP_SCOPE_INIT_INFO) * SCOPE_INIT_COUNT);

// Combine multiple scope types in a single array entry.

aScopeInit[0].cbSize = sizeof(DSOP_SCOPE_INIT_INFO);
aScopeInit[0].flType = DSOP_SCOPE_TYPE_UPLEVEL_JOINED_DOMAIN
                     | DSOP_SCOPE_TYPE_DOWNLEVEL_JOINED_DOMAIN;

// Set uplevel and downlevel filters to include only computer objects.
// Uplevel filters apply to both mixed and native modes.
// Notice that the uplevel and downlevel flags are different.

aScopeInit[0].FilterFlags.Uplevel.flBothModes =
        DSOP_FILTER_COMPUTERS;
aScopeInit[0].FilterFlags.flDownlevel =
        DSOP_DOWNLEVEL_FILTER_COMPUTERS;

// Initialize the DSOP_INIT_INFO structure.

ZeroMemory(&InitInfo, sizeof(InitInfo));

InitInfo.cbSize = sizeof(InitInfo);
InitInfo.pwzTargetComputer = NULL;  // Target is the local computer.
InitInfo.cDsScopeInfos = SCOPE_INIT_COUNT;
InitInfo.aDsScopeInfos = aScopeInit;
InitInfo.flOptions = DSOP_FLAG_MULTISELECT;

// You can call Initialize multiple times; last call wins.
// Note that object picker makes its own copy of InitInfo.

return pDsObjectPicker->Initialize(&InitInfo);
}

void ProcessSelectedObjects(
    IDataObject *pdo  // Data object containing user's selections
)
{

CHAR *szText = new CHAR[MAX_PATH*3];
HRESULT hr = S_OK;
BOOL fGotStgMedium = FALSE;
PDS_SELECTION_LIST pDsSelList = NULL;
ULONG i;
STGMEDIUM stgmedium = {
    TYMED_HGLOBAL,
    NULL,
    NULL
};
FORMATETC formatetc = {
    g_cfDsObjectPicker,
    NULL,
    DVASPECT_CONTENT,
    -1,
    TYMED_HGLOBAL
};

do {
    // Get the global memory block containing the user's selections.

    hr = pdo->GetData(&formatetc, &stgmedium);
    if (FAILED(hr)) break;
    fGotStgMedium = TRUE;

    // Retrieve pointer to DS_SELECTION_LIST structure.

    pDsSelList = (PDS_SELECTION_LIST) GlobalLock(stgmedium.hGlobal);
    if (!pDsSelList) break;

    // Loop through DS_SELECTION array of selected objects.

    for (i = 0; i < pDsSelList->cItems; i++) {
        wsprintf(szText,"Object: %u\nName: %ws\nClass: %ws\nADsPath: %ws",
            i,
                pDsSelList->aDsSelection[i].pwzName,
                pDsSelList->aDsSelection[i].pwzClass,
                pDsSelList->aDsSelection[i].pwzADsPath);
    MessageBox(NULL,szText,"Selection",MB_OK);
    }

    GlobalUnlock(stgmedium.hGlobal);
} while (0);

if (fGotStgMedium)
    ReleaseStgMedium(&stgmedium);

}


Domain Browser

Using the IDsBrowseDomainTree COM object, an application can display a domain browser dialog box and get back the DNS name of the domain selected by the user. The IDsBrowseDomainTree interface also has other methods to enumerate and retrieve information about all domain trees and domains within a forest.

The following function displays the domain browser dialog box where the user selects a domain. The function returns the DNS name of the domain:

//Need to include DSCLIENT.H
//Must link dsuiext.lib

HRESULT BrowseForDomain(HWND hWnd,//Handle to window that should own the browse dialog.
                LPOLESTR *ppDomainDNSName //Return the DNS name of selected domain.
                    )
{
HRESULT hr = E_FAIL;
IDsBrowseDomainTree *pDsDomains = NULL;
//CoCreate the domain tree browser.
hr = ::CoCreateInstance(CLSID_DsDomainTreeBrowser,
                NULL,
                CLSCTX_INPROC_SERVER,
                IID_IDsBrowseDomainTree,
                (void **)(&pDsDomains));
if (SUCCEEDED(hr))
{
hr = pDsDomains->BrowseTo(
                hWnd, //Handle to window that owns the dialog.
                ppDomainDNSName, //String to return DNS Name.
                0L //Flags
            );
//User must free string with CoTaskMemFree.
}
if (pDsDomains)
  pDsDomains->Release();
return hr;
}


Container Browser

Using the DsBrowseForContainer function, an application can display a container browser dialog box and get back the path to the container selected by the user (ADS_FORMAT_X500_NO_SERVER is the default—see the ADS_FORMAT enumeration for the list of all formats) and optionally the ldapDisplayName of the container's object class. The function takes a pointer to a DSBROWSEINFO structure that contains the caption and dialog box text, ADsPath to the root of the browser tree, credentials (user and password), and flags to control the appearance of the dialog box.

The following function displays the container browser dialog box where the user selects a container. The function returns the ADsPath and class of the container:

//Must include DSCLIENT.H
//Must link dsuiext.lib

HRESULT BrowseForContainer(HWND hWnd,//Handle to window that should own the browse dialog.
                    LPOLESTR szRootPath, //Root of the browse tree. NULL for entire forest.
                    LPOLESTR *ppContainerADsPath, //Return the ADsPath of the selected container.
                    LPOLESTR *ppContainerClass //Return the ldapDisplayName of the container's class.
                    )
{
HRESULT hr = E_FAIL;
DSBROWSEINFO dsbi;
OLECHAR szPath[MAX_PATH*2];
OLECHAR szClass[MAX_PATH];
DWORD result;

if (!ppContainerADsPath)
  return E_POINTER;

::ZeroMemory( &dsbi, sizeof(dsbi) );
dsbi.hwndOwner = hWnd;
dsbi.cbStruct = sizeof (DSBROWSEINFO);
dsbi.pszCaption = "Browse for Container"; // The caption (titlebar text)
dsbi.pszTitle = "Select a container."; //Text for the dialog.
dsbi.pszRoot = szRootPath; //ADsPath for the root of the tree to display in the browser.
                //Specify NULL with DSBI_ENTIREDIRECTORY flag for entire forest.
                //NULL without DSBI_ENTIREDIRECTORY flag displays current domain rooted at LDAP.
dsbi.pszPath = szPath; //Pointer to a unicode string buffer.
dsbi.cchPath = sizeof(szPath)/sizeof(OLECHAR);//count of characters for buffer.
dsbi.dwFlags = DSBI_INCLUDEHIDDEN | //Include hidden objects
            DSBI_IGNORETREATASLEAF| //Ignore the treat as leaf flag on the object for display purposes.
            DSBI_RETURN_FORMAT | //Return the path to object in format specified in dwReturnFormat
            DSBI_RETURNOBJECTCLASS; //Return the object class
dsbi.pfnCallback = NULL;
dsbi.lParam = 0;
dsbi.dwReturnFormat = ADS_FORMAT_X500; //Specify the format.
                    //This one returns an ADsPath. See ADS_FORMAT enum in IADS.H
dsbi.pszObjectClass = szClass; //Pointer to a unicode string buffer.
dsbi.cchObjectClass = sizeof(szClass)/sizeof(OLECHAR);//count of characters for buffer.

//if root path is NULL, make the forest the root.
if (!szRootPath)
  dsbi.dwFlags |= DSBI_ENTIREDIRECTORY;

//Display browse dialog box.
result = DsBrowseForContainer( &dsbi ); // returns -1, 0, IDOK or IDCANCEL
if (result == IDOK)
{
    //Allocate memory for string
    *ppContainerADsPath = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szPath)+1));
    if (*ppContainerADsPath)
    {
        wcscpy(*ppContainerADsPath, szPath);
        //Caller must free using CoTaskMemFree
        hr = S_OK;
    }
    else
        hr=E_FAIL;
    if (ppContainerClass)
    {
        //Allocate memory for string
        *ppContainerClass = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szClass)+1));
    if (*ppContainerClass)
    {
        wcscpy(*ppContainerClass, szClass);
        //Call must free using CoTaskMemFree
        hr = S_OK;
    }
    else
        hr=E_FAIL;
    }
}
else
    hr = E_FAIL;

return hr;

}


How Applications Should Use Display Specifiers

Most of this chapter has discussed how the Windows shell and Active Directory administrative snap-ins take advantage of display specifiers. How should you use them in your application?

  1. For labels for class and attribute names, use the classDisplayName and attributeDisplayNames properties of the display specifier objects for the appropriate locale. This enables you to take advantage of the localized display names and avoid doing unnecessary localization of the class and attribute names. Display specifiers also provide a single location (that is, in the directory) for localized text labels for class and attribute names.

    Do not use the cn, classDisplayName, ldapDisplayName properties on the classSchema or attributeSchema objects to display text labels in your applications—these properties are not localized.

  2. Use creation wizards to create new objects.
  3. Using localized icons.


Localization

Each locale container will hold Display-Specifier objects that have been localized for that locale. Active Directory UI applications will first look in a locale container named after the locale identifier for the current user's session. If a folder of that name is not found, the US-English locale will be used.

COM objects can be localized either by having a separate binary for each language or by having multiple language resources in a single binary.

Similarly, the class and attribute display names will be translated. Different icons can be specified for each locale.


User Interface Extension for New Object Classes

When creating a new class, you determine how objects of that class will be displayed. The classSchema object itself has properties that determine when a object is visible in the user interface. To provide class display name, attribute display names, icons, context menus, property pages, and creation wizards for the class in Active Directory administrative snap-ins and the Windows shell, you need to create a display specifier for the class. Note that you need to create a display specifier for each locale your class supports.

When you create a new class, consider the following settings and their effects on how the Windows shell and Active Directory administrative snap-ins will display objects of that class:

defaultHidingValue
If you do not set defaultHidingValue, it defaults to TRUE. This means that instances of the object are hidden in the Administrative snap-ins and the Windows shell. It also means that a menu item for the new object class will not appear in the New context menu of the Administrative snap-ins—even if the appropriate creation wizard properties are set on the new object class's displaySpecifier object.

Set this property to FALSE if you want to be able to see instances of the class in the administrative snap-ins and the shell and enable a creation wizard and its menu item in the New menu of the administrative snap-ins.

showInAdvancedViewOnly
If you do not set showInAdvancedViewOnly, it defaults to TRUE.

When set to TRUE, instances of the object appear in the Users and Computers snap-in in Advanced View only and do not appear in the Windows shell.

Set this property to FALSE if you want to be able to see instances of the class in Normal view in the Users and Computers snap-in and the Windows shell.

Note that you can also set this value on an individual object to override the value set on the object's class. For example, the container class has this value set to TRUE but the Users container has this value set to FALSE; therefore, the Users container appears in the shell and in Normal view in the Users and Computers snap-in but other containers that do not have showInAdvancedViewOnly set to FALSE will appear only in Advanced view in the Users and Computers snap-in.


Creating Display Specifiers for New Classes

Users of Active Directory can customize it to suit their unique requirements. The user interface can also be changed to suit their needs. Active Directory permits the schema to be modified by creating new classes and attributes or modifying existing classes. Display Specifiers can be modified to reflect the new user interface elements that schema modifications require.

A Display-Specifier object is created for the new class. If multiple locales are present, new Display-Specifier objects are created for each supported locale. If the new class is derived from a parent class and the parent class already has an acceptable UI, that UI is specified and additional pages, menu items, and display names can be added for the new attributes. Otherwise, a completely new UI is created and specified. In either case, a new icon and class display name can be used.

Note that a new class that inherits from an existing class does not inherit the parent class's display specifier. If you want the new class to use some or all of the parent class's display specifier properties, you must create a new display specifier for the new class and copy the properties you want from the parent class's display specifier to the new display specifier. This must be done for all locales for which you want to use the parent class's display specifier properties.


Modifying Existing Classes

New attributes can be added to an existing class. New UI components (pages, menu items, and attribute display names) can be added or the existing UI replaced. It is also possible to design new property pages that expose fewer attributes of a class and to create context menus with fewer actions.


Extending Active Directory Adminstrative Snap-ins using MMC Extension Snap-ins

MMC enables you to create extension snap-ins that extend the namespace, context menus, property pages, control bars, toolbars, and taskpads of other snap-ins. Active Directory administrative snap-ins are standalone snap-ins that can be extended by extension snap-ins.

The following tutorial provides all the steps required to create an MMC context menu extension.


Context Menu Extension Tutorial

Using the conventional MMC extension snap-in mechanisms, only the All Tasks and Create New context menus can be extended.

  1. Register the node type for the object class whose context menu you want to extend.

    Note that the node type that you want the snap-in to extend must be registered in the MMC registry key. For more information on registering a node type for nodes (objects of a specific class) in Active Directory Manager, see Registering Node Types for Active Directory Manager.

  2. Create a new project using the ATL COM AppWizard with the server type Dynamic Link Library (DLL).

    For this tutorial, type MyExtensionSnapin as the Project Name.

    To create a new project, choose New from the File menu, select the Projects tab, select ATL COM AppWizard, type the project name in the Project Name box, and click OK. The ATL COM AppWizard page appears. Select Dynamic Link Library (DLL) and click Finish. For this tutorial, do not select the other options in wizard page. The New Project Information dialog box appears. Click OK.

  3. Add an MMC Snapin object by choosing New ATL Object from the Insert menu.
  4. In the ATL Object Wizard dialog box, select Objects from the Category list and MMC SnapIn from the Objects list. Click Next.
  5. In the ATL Object Wizard Properties dialog box, select the Names tab and type the control name in the Short Name box. The other boxes are filled in based on this name. For this tutorial, type MyContextMenu in the Short Name box.
  6. Select the MMC Snapin tab and make the following settings:
    1. Check the Extension box, click on the browse (…) button for Extends Node, and select the node type from the Snap-In Nodes dialog box. For this tutorial, select BF967ABA-0DE6-11D0-A285-00AA003049E2 in the Snap-In Nodes dialog box so that this snap-in extends the user class.

      Note that the node type that you want the snap-in to extend must be registered in the MMC registry key. For more information on registering a node type for nodes (objects of a specific class) in Active Directory Manager, see Registering Node Types for Active Directory Manager.

    2. Make sure that the IComponentData, IComponent, ISnapInAbout, and IExtendContextMenu are checked. If you want to add a property sheet or control bar, check those options as well. For this tutorial, just select the specified four items: IComponentData, IComponent, ISnapInAbout, and IExtendContextMenu.
    3. Clear the Supports Persistence box. If you want to save the state of your snap-in, you can use the persistence interfaces. For this tutorial, clear the Supports Persistence box.
  7. Click OK. The CMyContextMenuExtData class is created. This class implements the extension snap-in. A menu resource is also created.

    Note that the following classes are also created: CMyContextMenuData (implements IComponentData, which interacts with scope pane), CMyContextMenuComponent (implements IComponentData, which interacts with result pane), and CMyContextMenuAbout (implements ISnapinAbout, which controls About box information and some information in the Add/Remove Snap-ins dialog box).

  8. Open the menu resource by clicking on the Resource tab, expanding the MyExtensionSnapin resources folder, expanding the Menu folder, and double-clicking the menu resource IDR_MYCONTEXTMENU_MENU. The menu has four menu items: TOP, NEW, TASK, and VIEW.

    Note that an extension snap-in can only add menu items to the NEW and TASK menus.

  9. Add a menu item to the TASK menu by clicking on TASK and double-clicking on the empty menu item that appears beneath TASK. The Menu Item Properties box appears.
  10. In the Caption box, type the text for the menu item.

    For this tutorial, type the following: Do Some Extension Thing.

  11. In the Prompt box, type the description for the menu item that you want to display in the status bar when the menu item is selected.

    For this tutorial, type the following: Does some extension thing.

  12. 12. In the ID box, type the command ID for the menu item. You can leave this blank if you want an ID to be automatically generated.

    For this tutorial, type the following: ID_DO_SOMETHING.

  13. Close the Menu Item Properties dialog box.
  14. Open the header file containing the class definition of CMyExtensionSnapinExtData. Click the ClassView tab and then double-click CMyExtensionSnapinExtData.
  15. Find the following two lines:
    BEGIN_SNAPINCOMMAND_MAP(C CMyExtensionSnapinExtData ExtData, FALSE)
END_SNAPINCOMMAND_MAP()
  16. Change the FALSE parameter to TRUE to indicate that the snap-in is an extension. The line should look like this:
    BEGIN_SNAPINCOMMAND_MAP(CMyExtensionSnapinExtData, TRUE)
  17. Add the SNAPINCOMMAND_ENTRY macro between the two lines and specify the command ID for the menu item as the first parameter and the name of the method that serves as the command handler for the command ID as the second. The snap-in command map should now look like this:
    BEGIN_SNAPINCOMMAND_MAP(CMyExtensionSnapinExtData, TRUE)
    SNAPINCOMMAND_ENTRY(ID_DO_SOMETHING, OnDoSomeThing)
END_SNAPINCOMMAND_MAP()
  18. Add the command handler as a method in the class. The command handler must take the following form:

    HRESULT MethodName (bool& bHandled,

    CSnapInObjectRootBase* pObj)

    For this tutorial, add the following method to the class:

    HRESULT OnDoSomeThing (bool& bHandled,
    CSnapInObjectRootBase* pObj)
{
    ::MessageBoxW( NULL, L" OnDoSomeThing ", L" CMyExtensionSnapinExtData ", MB_OK);
    return S_OK;
}
  19. Build the project.
  20. Load the Active Directory Manager snap-in into MMC or open the DSA.MSC console file. Open the Active Directory Manager, then the domain node, then the Users container, right-click on a user such as Administrator, select All Tasks. The menu item Do Some Extension Thing should appear. Choose the menu item. The OnDoSomeThing method is called and the message box appears.


Registering Node Types for Active Directory Manager

A critical part of extending Active Directory Manager is registering the node type(s) that you want to extend. Note that Active Directory Manager does not register all the possible node types that it can display. Because the schema is extensible (new types of objects can be added as classes to the schema and those objects can be represented in the Active Directory Manager), the Active Directory Manager does not attempt to add every single class defined in the schema as a node type. For Active Directory Manager, the GUID for the node type is the GUID stored in the schemaIDGUID property of the classSchema object that represents the type of object that you want to extend. For example, if you wanted to add a context menu item to the Tasks menu of user objects in Active Directory Manager, you would use the COM string representation of GUID stored in the schemaIDGUID property of the user classSchema object as the node type GUID.

If the node type you want to extend is not registered, you must add the appropriate node type keys to the registry. The following keys must exist for the node type:

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes\{nodetype}

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\SnapIns\{snapinCLSID}\NodeTypes\{nodetype}

For Active Directory Manager, the {nodetype} key is the schemaIDGUID of the object class whose node you want to extend. The {snapinCLSID} key is the class ID of the Active Directory Manager snap-in, which is {E355E538-1C2E-11D0-8C37-00C04FD8FE93}.

For example, the following registry entries must exist for a taskpad extension snap-in (with CLSID of {897C0FB4-8850-11D2-9523-00C04F8607E2}) to extend the user objects:

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes\{BF967ABA-0DE6-11D0-A285-00AA003049E2}

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes\{BF967ABA-0DE6-11D0-A285-00AA003049E2}\extensions\Task\{897C0FB4-8850-11D2-9523-00C04F8607E2}

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\SnapIns\{E355E538-1C2E-11D0-8C37-00C04FD8FE93}\NodeTypes\{BF967ABA-0DE6-11D0-A285-00AA003049E2}

For information about reading the schemaIDGUID of classes and registering them as MMC node types for Active Directory Manager, see the sample code below. For more detail on MMC node types, see MMC node types.

The following program registers a class as a node type and adds an extension snap-in as a context menu extension to that node type:

  1. Finds the user classSchema object and gets its schemaIDGUID property, which will be used to register the node type for the user object class. This GUID is converted using the GetCOMGUIDStr function.
  2. Uses RegisterNodeType to register the user object class as a node type.
  3. Uses AddExtensionToNodeType to add an extension snap-in as a context menu extension with the CLSID of {275C0FB4-8850-11D2-9523-00C04F8607E2} to the user class object node type (which was previously created by RegisterNodeType).
#include <wchar.h>
#include <activeds.h>

#define MMC_REG_NODETYPES L"software\\microsoft\\mmc\\nodetypes"
#define MMC_REG_SNAPINS L"software\\microsoft\\mmc\\snapins"
#define MMC_REG_SNAPINS L"software\\microsoft\\mmc\\snapins"

//MMC Extension subkeys

#define MMC_REG_EXTENSIONS L"Extensions"
#define MMC_REG_NAMESPACE L"NameSpace"
#define MMC_REG_CONTEXTMENU L"ContextMenu"
#define MMC_REG_TOOLBAR L"ToolBar"
#define MMC_REG_PROPERTYSHEET L"PropertySheet"
#define MMC_REG_TASKPAD L"Task"

//DSADMIN key
#define MMC_DSADMIN_CLSID L"{E355E538-1C2E-11D0-8C37-00C04FD8FE93}"

WCHAR * GetDirectoryObjectAttrib(IDirectoryObject *pDirObject,LPWSTR pAttrName);

HRESULT GetCOMGUIDStr(LPOLESTR *ppAttributeName,IDirectoryObject *pDO, LPOLESTR *ppGUIDString);

HRESULT  RegisterNodeType( LPOLESTR pszSchemaIDGUID );

HRESULT  AddExtensionToNodeType(LPOLESTR pszSchemaIDGUID,
                    LPOLESTR pszExtensionType,
                    LPOLESTR pszExtensionSnapinCLSID,
                    LPOLESTR pszRegValue
                    );

int main(int argc, char* argv[])
{
LPOLESTR szPath = new OLECHAR[MAX_PATH];
HRESULT hr = S_OK;
IADs *pObject = NULL;
VARIANT var;
IDirectoryObject *pDO = NULL;
LPOLESTR pAttributeName = L"schemaIDGUID";
LPOLESTR pGUIDString = NULL;

wcscpy(szPath, L"LDAP://");
CoInitialize(NULL);
//Get rootDSE and the schema container's DN.
//Bind to current user's domain using current user's security context.
hr = ADsOpenObject(L"LDAP://rootDSE",
            NULL,
            NULL,
            ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
            IID_IADs,
            (void**)&pObject);

if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"schemaNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        wcscat(szPath, L"cn=user,");
        wcscat(szPath,var.bstrVal);
        if (pObject)
        {
            pObject->Release();
            pObject = NULL;
        }
        hr = ADsOpenObject(szPath,
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IDirectoryObject,
                (void**)&pDO);
        if (SUCCEEDED(hr))
        {
            hr = GetCOMGUIDStr(&pAttributeName,
                        pDO,
                        &pGUIDString);
        if (SUCCEEDED(hr))
        {
            wprintf(L"schemaIDGUID: %s\n", pGUIDString);
            hr = RegisterNodeType( pGUIDString);
            wprintf(L"hr %x\n", hr);
            hr = AddExtensionToNodeType(pGUIDString,
                        MMC_REG_CONTEXTMENU,
                        L"{275C0FB4-8850-11D2-9523-00C04F8607E2}",
                        L"MySnapin"
                        );

            }
        }
    }
}
if (pDO)
    pDO->Release();

VariantClear(&var);

// Uninitialize COM
CoUninitialize();
return 0;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    GetDirectoryObjectAttrib()      -   Returns the value of the attribute named in pAttrName
                                        from the IDirectoryObject passed
    Parameters

        IDirectoryObject *pDirObject    - Object from which to retrieve an attribute value
        LPWSTR pAttrName                - Name of attribute to retrieve
*/
WCHAR * GetDirectoryObjectAttrib(IDirectoryObject *pDirObject,LPWSTR pAttrName)
{
    HRESULT   hr;
    ADS_ATTR_INFO   *pAttrInfo=NULL;
    DWORD   dwReturn;
    static WCHAR pwReturn[1024];

    pwReturn[0] = 0l;

    hr = pDirObject->GetObjectAttributes( &pAttrName,
                                            1,
                                            &pAttrInfo,
                                            &dwReturn );
    if ( SUCCEEDED(hr) )
    {
        for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
        {
            if ( _wcsicmp(pAttrInfo->pszAttrName,pAttrName) == 0 )
            {
                wcscpy(pwReturn,pAttrInfo->pADsValues->CaseIgnoreString);
                break;
            }
        }
        FreeADsMem( pAttrInfo );
    }
    return pwReturn;
}

HRESULT GetCOMGUIDStr(LPOLESTR *ppAttributeName,IDirectoryObject *pDO, LPOLESTR *ppGUIDString)
{
    HRESULT hr = S_OK;
    PADS_ATTR_INFO  pAttributeEntries;
    VARIANT varX;
    DWORD dwAttributesReturned = 0;
    hr = pDO->GetObjectAttributes(  ppAttributeName, //objectGUID
                                  1, //Only objectGUID
                                  &pAttributeEntries, // Returned attributes
                                  &dwAttributesReturned //Number of attributes returned
                                );
    if (SUCCEEDED(hr) && dwAttributesReturned>0)
    {
        //Make sure that we got the right type--GUID is ADSTYPE_OCTET_STRING
        if (pAttributeEntries->dwADsType == ADSTYPE_OCTET_STRING)
        {
            LPGUID pObjectGUID = (GUID*)(pAttributeEntries->pADsValues[0].OctetString.lpValue);
            //OLE str to fit a GUID
            LPOLESTR szDSGUID = new WCHAR [39];
            //Convert GUID to string.
            ::StringFromGUID2(*pObjectGUID, szDSGUID, 39);
        *ppGUIDString = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szDSGUID)+1));
        if (*ppGUIDString)
            wcscpy(*ppGUIDString, szDSGUID);
        else
            hr=E_FAIL;
    }
    else
        hr = E_FAIL;
    //Free the memory for the attributes.
    FreeADsMem(pAttributeEntries);
    VariantClear(&varX);
    }
    return hr;
}

HRESULT  RegisterNodeType(LPOLESTR pszSchemaIDGUID)
{
    LONG     lResult;
    HKEY     hKey;
    HKEY     hSubKey, hNewKey;
    DWORD    dwDisposition;
    LPOLESTR szRegSubKey = new OLECHAR[MAX_PATH];

        // first, open the HKEY_LOCAL_MACHINE
        lResult  = RegConnectRegistry( NULL, HKEY_LOCAL_MACHINE, &hKey );
        if ( ERROR_SUCCESS == lResult )
    {
        //go to the MMC_REG_NODETYPES subkey
            lResult  = RegOpenKey( hKey, MMC_REG_NODETYPES, &hSubKey );
            if ( ERROR_SUCCESS == lResult )
        {
            // Create a key for the node type of the class represented by pszSchemaIDGUID
            lResult  = RegCreateKeyEx( hSubKey,                // handle of an open key
                        pszSchemaIDGUID,       // address of subkey name
                        0L ,                    // reserved
                        NULL,
                        REG_OPTION_NON_VOLATILE,// special options flag
                        KEY_ALL_ACCESS,
                        NULL,
                        &hNewKey,
                        &dwDisposition );
            RegCloseKey( hSubKey );
        if ( ERROR_SUCCESS == lResult )
        {
            hSubKey = hNewKey;
                // Create an extensions key
            lResult  = RegCreateKeyEx( hSubKey,
                    MMC_REG_EXTENSIONS,
                                0L ,
                                NULL,
                                REG_OPTION_NON_VOLATILE,
                                KEY_ALL_ACCESS,
                                NULL,
                                &hNewKey,
                                &dwDisposition );
            //go to the MMC_REG_SNAPINS subkey
            RegCloseKey( hSubKey );
            //Build the subkey path to the NodeTypes key of dsadmin
            wcscpy(szRegSubKey, MMC_REG_SNAPINS); //Snapins key
            wcscat(szRegSubKey, L"\\");
            wcscat(szRegSubKey, MMC_DSADMIN_CLSID); //CLSID for DSADMIN
            wcscat(szRegSubKey, L"\\NodeTypes");
            lResult  = RegOpenKey( hKey, szRegSubKey, &hSubKey );
            if ( ERROR_SUCCESS == lResult )
            {
                // Create a key for the node type of the class represented by pszSchemaIDGUID
                lResult  = RegCreateKeyEx( hSubKey,                // handle of an open key
                                pszSchemaIDGUID,       // address of subkey name
                                0L ,                    // reserved
                                NULL,
                                REG_OPTION_NON_VOLATILE,// special options flag
                                KEY_ALL_ACCESS,
                                NULL,
                                &hNewKey,
                                &dwDisposition );
                    RegCloseKey( hSubKey );
                }

            }
        }
    }
    RegCloseKey( hSubKey );
    RegCloseKey( hNewKey );
    RegCloseKey( hKey );
        return lResult;
}

HRESULT  AddExtensionToNodeType(LPOLESTR pszSchemaIDGUID,
                        LPOLESTR pszExtensionType,
                        LPOLESTR pszExtensionSnapinCLSID,
                        LPOLESTR pszRegValue
                        )
{
        LONG     lResult;
        HKEY     hKey;
        HKEY     hSubKey, hNewKey;
        DWORD    dwDisposition;
    LPOLESTR szRegSubKey = new OLECHAR[MAX_PATH];
    HRESULT hr = S_OK;

        // first, open the HKEY_LOCAL_MACHINE
        lResult  = RegConnectRegistry( NULL, HKEY_LOCAL_MACHINE, &hKey );
        if ( ERROR_SUCCESS == lResult )
    {
        //Build the subkey path to the NodeType specified by pszSchemaIDGUID
    wcscpy(szRegSubKey, MMC_REG_NODETYPES);
    wcscat(szRegSubKey, L"\\");
    wcscat(szRegSubKey, pszSchemaIDGUID);
    //go to the subkey
        lResult  = RegOpenKey( hKey, szRegSubKey, &hSubKey );
        if ( ERROR_SUCCESS != lResult )
    {
        // Create the key for the nodetype if it doesn't already exist.
        hr = RegisterNodeType(pszSchemaIDGUID);
            if ( ERROR_SUCCESS != lResult )
            return E_FAIL;
            lResult  = RegOpenKey( hKey, szRegSubKey, &hSubKey );
    }
    // Create an extensions key if one doesn't already exist
    lResult  = RegCreateKeyEx( hSubKey,
                    MMC_REG_EXTENSIONS,
                               0L ,
                               NULL,
                               REG_OPTION_NON_VOLATILE,
                               KEY_ALL_ACCESS,
                               NULL,
                               &hNewKey,
                               &dwDisposition );
    RegCloseKey( hSubKey );
    if ( ERROR_SUCCESS == lResult )
    {
        hSubKey = hNewKey;
        // Create an extension type subkey if one doesn't already exist
        lResult  = RegCreateKeyEx( hSubKey,
                    pszExtensionType,
                           0L ,
                           NULL,
                           REG_OPTION_NON_VOLATILE,
                           KEY_ALL_ACCESS,
                           NULL,
                           &hNewKey,
                           &dwDisposition );
        RegCloseKey( hSubKey );
        if ( ERROR_SUCCESS == lResult )
            {
            hSubKey = hNewKey;
            // Add your snap-in to the
            //extension type key if it hasn't been already.
            lResult  = RegSetValueEx( hSubKey,
                pszExtensionSnapinCLSID,
                           0L ,
                           REG_SZ,
                           (const BYTE*)pszRegValue,
                           (wcslen(pszRegValue)+1)*sizeof(OLECHAR)
                    );
            }

        }
 }
    RegCloseKey( hSubKey );
    RegCloseKey( hNewKey );
    RegCloseKey( hKey );
    return lResult;
}


MMC Node Types

All node types that can be extended have their own subkey in the HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes key. A node type key contains an Extensions subkey. The Extensions key contains subkeys that represent the types of extensions (such as NameSpace for namespace extensions, and so on). Each extension type subkey contains values that represent the CLSIDs of the snap-ins that extend that node type with that type of extension. For example, you would add the CLSID of a snap-in that extended the namespace of the node type in the MMC\NodeTypes\{nodeguid}\Extensions\NameSpace subkey.

MMC uses the NodeTypes key to determine the extension snap-ins that can extend the node types for each snap-in. An extension snap-in must register itself for each node type that it extends as well as register what type of extensions it provides.

A snap-in with node types that can be extended must add those node types as node type keys in the HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes key. A node type key is represented as a key whose name is the GUID of the node type. The node type must also be added as a value in the snap-in's HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\SnapIns\{snapinCLSID}\NodeTypes key.

An extension snap-in must add its CLSID as a value to the extension type key for each type of extension it provides for a particular node type.

The NodeTypes key has the following form:

HKEY_LOCAL_MACHINE\Software\Microsoft\MMC\NodeTypes\
    {nodetypeGUID}
        Extensions
            NameSpace
                {extensionsnapinCLSID}
            ContextMenu
                {extensionsnapinCLSID}
            ToolBar
                {extensionsnapinCLSID}
            PropertySheet
                {extensionsnapinCLSID}
            Task
                {extensionsnapinCLSID}
        Dynamic Extensions
            {extensionsnapinCLSID}


Replication and Data Integrity

Active Directory™ provides multi-master update. Multi-master update means that all full replicas of a given partition are writeable (the partial replicas on global catalog servers are not writeable.) Multi-master update means that updates are not blocked even when some replicas are down. Active Directory propagates the changes from a the updated replica to all other replicas. Replication is automatic and transparent.


Active Directory Replication Model

This section discusses the following:

  • What is the Active Directory Replication Model?
  • Why Active Directory uses This Replication Model?
  • A Programmer's Model of Active Directory Replication


What is the Active Directory Replication Model?

The replication model used in Active Directory is called multi-master loose consistency with convergence. In this model, the directory can have many replicas; a replication system propagates changes made at any given replica to all other replicas. The replicas are not guaranteed to be consistent with each other at any particular point in time ("loose consistency"), since changes can be applied to any replica at any time ("multi-master"). If the system is allowed to reach a steady state, in which no new updates are occurring and all previous updates have been completely replicated, all replicas are guaranteed to converge on the same set of values ("convergence").


Why Active Directory Uses This Replication Model

Active Directory is a loosely coupled system for the following reasons:

  • Customers require a highly distributed solution in which parts of the directory can be spread across their networks and administered locally.
  • Large customers need to grow to many millions of objects, hundreds or thousands of replicas, or both.
  • Many customer networks provide only intermittent connectivity to some locations, for example, remote oil drilling platforms and ships at sea, so the system must be tolerant of partly connected or disconnected operations.

There is no way to guarantee complete knowledge of the current or future state of a distributed system, because knowledge of state changes must be propagated and propagation takes time, during which more state changes may occur. This is an axiom of distributed computing.

Tightly coupled systems deal with uncertainty by attempting to eliminate it. This is done through constraints on updates, requiring all nodes or some majority of nodes to be available before updates can be performed, using distributed locking schemes or single-mastering for critical resources, constraining all nodes to be well connected, or some combination of these techniques. The more tightly coupled the computing nodes in a distributed system are, the lower the scaling limit.

Loosely coupled systems deal with uncertainty by tolerating it. A loosely coupled system allows the nodes to have differing views of the overall system state and provides algorithms for resolving conflicts.

Tightly coupled solutions were rejected as unsuitable for Active Directory because of the requirements for local administration, disconnected operation, and scalability to very large numbers of nodes. The loosely coupled model chosen, multi-master loose consistency with convergence satisfies all of these requirements.


A Programmer's Model of Active Directory Replication

The following does not describe how Active Directory replication works – instead it gives a much simpler model that's equivalent from an application programmer's perspective.

All updates to Active Directory are performed using LDAP requests that create, modify, or delete one object per request. A single request can set or modify any number of attributes on an object.

An update request is processed as an atomic transaction at some domain component (DC). Either the entire update happens or none of it does. If the requester receives a successful response to an update request, that entire request has succeeded (committed.) That's called an originating write. You can't group multiple LDAP requests into a single larger transaction.

As part of performing an originating write, a DC computes a "stamp" for each new or modified attribute value, and attaches this stamp to the value so when the value is replicated, the stamp is replicated too. The new stamp is guaranteed to be different from all other stamps, and in case of an update the new stamp is always larger than the stamp on the old value at that DC.

Occasionally, a DC selects the set of objects that have changed since the last time the DC performed replication. Then, for each object, it sends a single message to all other DCs containing all the current values of attributes changed since the last time the object was replicated. Replication messages are reliable and are delivered in order but may take a long time to be delivered.

When one DC receives a replication message from another DC, it processes it as follows: For each modified attribute, if the stamp on the value in the replication message is larger than the stamp on the current value, the DC applies the update; otherwise the DC discards the update. Each replication message is applied as an atomic transaction, just like an originating write.

That's the complete model of how Active Directory replication works. Key properties of this model:

  • An originating write to a single object is atomic.
  • When replicating changes, either all the changes made by an originating write are sent, or none of them are.
  • A replicated write to a single object is atomic, but conflicts are resolved attribute-by-attribute.

The model does not guarantee the replication ordering of changes made to different objects. Don't write applications that assume changes will be replicated in originating-write order. The model does not guarantee that if an attribute of an object is changed twice, both values will be replicated: Replication sends only the current value at the time of replication.

The model differs from reality in several ways that only affect performance. For instance, Active Directory actually sends replication messages containing the changes to multiple objects; but it processes the contents of such a multi-object message as if it were a series of single-object messages. And Active Directory does not perform point-to-point replication as described in the model, but instead performs a more complex and more efficient transitive replication that is functionally equivalent to the model.


Active Directory Replication Behavior

Replication behavior is consistent and predictable; given a set of changes to a given replica the outcome can be predicted — the changes will be propagated to all other replicas. Devising a reliable general model for predicting when the changes will be applied at all other replicas, or at a particular replica, is impossible, because the future state of the distributed system as a whole cannot be known. This is called nondeterministic latency, and applications that use the directory must understand and allow for it.

The situation is not as complex at it might appear. There are only 3 states that an application must accommodate:

  • Version Skew. This is when none of the changes applied to a given source replica have propagated to a given destination replica. An application reading the source replica sees the new version of the information, while an application reading the destination sees the old version (or nothing, if the new information was added for the first time). Version skew applies to all directory service consumers.
  • Partial Update. This is when some of the changes applied to a given source replica have propagated to a given destination replica. An application reading the source replica sees the new information, while an application reading the destination sees a mix of old and new (or only some of the new, if the new information was added for the first time). Partial update applies to directory service consumers that use 2 or more related objects to store their information.
  • Fully Replicated State. All of the changes applied to a given source replica have propagated to a given destination replica. Applications at the source and destination replicas see the same information.


Impact on Directory-Enabled Applications

Version Skew

Version skew occurs when applications read the same object(s) from different replicas before a change has replicated. Applications reading the remote replica see the unchanged object. Version skew is an issue when a given application or set of applications use the information in the directory to interoperate.

For example, an RPC Service can publish its endpoints in the directory using standard RPC APIs (such as RpcNsBindingExport). Clients connect to the service by looking up the desired endpoint in the directory (RpcNsBindingLookupBegin, RpcNsBindingLookupNext, and so on) and binding to it.

Assume that an RPC Service S1 publishes endpoint Es1 and subsequently moves to a different computer. The original endpoint Es1 is changed to Es2, reflecting the new computer's address. Clients reading remote replicas of the directory service are unable to connect to the service until the updated endpoint is replicated. However, moving a service from one computer to another is a rare occurrence; therefore, this interruption/delay should rarely be encountered, especially if the movement of the service is well planned.

Partial Updates

In general, partial update occurs when one application is reading a set of data while another application is writing to that same set of data. This is a situation that can occur with any application in a multi-mastered system. There are many ways this can occur. You could have n applications writing to the same data set at once. If you look at it this way, the directory replication service is just another application that could potentially be writing to the same data set that another application may be reading. This could be a potential problem for an application. However, the window of time in which a partial update can affect your application is relatively small. This should rarely if ever be an issue for applications that are not dependent on the synchronization of multiple objects. If your application is highly dependent on the synchronization of a related set of objects, you should consider the effects of a partial update in your application design.

In terms of the directory replication, partial update occurs when applications read the same set of objects from different replicas while replication is in progress. Applications at the remote replica see some of the changes but not all.

Note that the window in which partial update can affect an application is small: the application must start reading objects while inbound replication is in progress, after one or more of the related, changed objects have been received but before all have been received. The time between the updates at the source replica directly affects the size of this window — updates that occur close together in time will be replicated close together in time. Partial update can be an issue when an application uses a related set of objects.

For example, a remote access service can use the directory to store policy and profile information. The policy information is stored in one set of objects, and the profile in another set. When a user connects to the remote access service, the remote access service reads the policy to determine whether the user is allowed to connect, and if so what profile to apply to the user's session. Partial update can affect the remote access service in several ways:

  • If the policy is complex and consists of multiple objects, the remote access service might read a partially updated policy resulting in incorrect denial or granting of service to the user, inability to process the policy due to internal inconsistency, and so on
  • If the both policy and profiles have been updated, the service might correctly process the policy but apply a stale profile, because the policy objects have replicated but the profile objects have not.
  • If the profile is complex and consists of multiple objects, the service might correctly process the policy but apply a partially updated profile because the policy objects have replicated but only some of the profile objects have done so.

Collisions

Collisions occur when the same attributes of two or more replicas of a given object are changed during the same replication interval. The replication process reconciles the collision; because of reconciliation a user or application may "see" a value other than the one they wrote.

A simple example is user address information: A user changes their mailing address at replica R1 and an administrator changes the same mailing address at replica R2. A written value propagates until another value is selected over that value by the collision reconciliation mechanism. As long as a value continues to "win" against other values in the collision resolution process, the value continues to propagate. Ultimately, this "winning" value will be propagated to R1,R2, and all other replicas if no other changes are made.

Collision resolution is an issue for applications that make assumptions about the internal consistency of objects or sets of objects. For example, a network bandwidth allocation management service stores network bandwidth information for a given network segment in the directory in an object O1. The object contains the bandwidth available in bits-per-second and the maximum bandwidth any single user can reserve. The service expects that the user reservable bandwidth will always be less than or equal to the bandwidth available.

  • The object in the initial fully replicated state gives the available bandwidth as 56k bits-per-second, and the user reservable bandwidth as 9600 bits-per-second.
  • An administrator at replica R1 changes the values to 64k and 19,200 respectively.
  • An administrator at replica R2 changes the values to 10 million and 1 million respectively before the update from R1 arrives.

Assuming the attributes in question have equal version numbers when the updates occur, there is a small but real possibility that the object will end up with a maximum bandwidth of 64k and a user reservable maximum of 1 million — if the application performs the updates as separate write operations The application should always update both properties in a single operation.


Detecting and Avoiding Replication Latency

Replication latency is a fact of life in a loosely coupled distributed system. Applications must accommodate this. The best way to accommodate replication latency is to design applications to minimize the effects. The ideal directory-enabled application:

  • Is insensitive to version skew
  • Does not depend on relationships among multiple objects
  • Has no intra- or inter-object consistency requirements

Applications and services that fit this profile need not be concerned with replication latency. Other applications must be designed with replication latency in mind. The key to success in designing such an application is awareness of the replication process. Steps taken at design time to reduce inter-object dependencies and minimize partial update windows will pay large dividends at run time. Approaches to dealing with replication latency are divided into two classes — avoidance strategies that reduce the impact of latency and detection strategies that allow an application to detect latency-induced states.


What Can You Know, and When Can You Know It?

Applications that are sensitive to latency-induced states must recognize problem states and take appropriate action. There are two problem states: version skew and partial update. Version skew is not detectable by examining the Directory Service (remember the axiom of distributed computing stated in Why Active Directory Uses This Replication Model). Partial update can be detected by adding metadata to the objects that compose the related set. Suggestions for such metadata appear in subsequent sections of this document. There are avoidance strategies applications can take to reduce the possibility of both version skew and partial update, these strategies are also discussed in subsequent sections of this document.


Temporal Locality

Temporal Locality is an avoidance strategy that reduces the window for partial updates. Replication of changes from a given source replica proceeds in ascending USN order. Writes that occur close together in time will have USNs that are "near" each other, and will be propagated close together in time. Applications that create or update multiple, related objects should write the objects as close together in time as possible. For example, the application could gather all necessary input from the user and apply it to the directory when the user clicks an "Apply" button.

Temporal Locality also reduces the odds of collision resolution introducing intra-object inconsistencies.


Out-of-Band Signaling

Cooperating applications that share common data using the directory can use out-of-band signaling to avoid both version skew and partial updates. Put simply, the cooperating applications use a mechanism separate from directory replication to inform each other of changes in the shared common data. Out-of-band signaling is most effective when used in combination with a versioning mechanism — this allows the partner detecting the change to notify remote partners what version of the shared data to wait for.

Returning to the RPC example given in "Version Skew," the RPC server could call back into connected clients to inform them of the move to a new server ("I am going to move. Please stand by, replication will bring you the new address shortly") so that the client can handle the transition period.

Applications that require identical policies to be in force in order to communicate with each other can use out-of-band signaling to ensure that a new policy is not employed until all involved parties have received it. For example, if the IP Security policy between two machines is changed, an agent on the source machine could contact an agent on the destination machine to negotiate the application of the changed policy, with the source machine delaying application until the destination machine has received the new policy as well.


Effective Date and Time

Effective Date and Time is an avoidance strategy that prevents version skew and partial updates by deferring the effective data of information to some point in the future when the change has a very high probability of being fully replicated. To implement effective dates, the application objects must include an effective date timestamp, which is filled in when the object is created or changed. Consumers of the object(s) check the timestamp and defer use of the object(s) until that date and time are reached.

Some important considerations:

  • Applications that choose this approach must ensure that there is a set of applicable data to use until the updated objects become effective.
  • Distributed time service in Windows NT® 4.0 keeps the clocks of the connected Windows® 2000 systems synchronized. No time service is perfect, however, so there is a small window for version skew.
  • Setting a good effective date presumes knowledge of the overall replication latency for the distributed system in question. In a stable network a good rule of thumb for effective dates is the time of the update plus 2*(average overall latency). So, for a system whose overall latency averages 4 hours, a delay of 8 hours is reasonable.
  • In an unstable network determining a "good" value for effective dates is much more difficult, since the latency may be highly variable. Effective date is most "effective" in an unstable network when combined with other avoidance or detection strategies such as checksums or consistency GUIDs (see the next section for more information).


Checksums and Object Counts

Checksums and Object Counts are detection strategies that allow an application to detect a partial update state. Checksums can also be used to detect inconsistencies introduced by collision resolution. Both checksums and object counts require a place to store the value used for verifying a checksum or object count. This can be on a "master" object chosen from among those involved in the application-specific relationship or on a parent object under which the related objects are stored.

For checksums, applications reading the related objects verify the checksum by calculating a local result and comparing it with the stored value. If the values do not match the replica is in a partial update state and the objects cannot be used.

For object counts, applications count the related objects (typically children of a single parent) and compare the count with the stored value. If the counts do not match the replica is in a partial update state and the objects cannot be used.

Some important considerations:

  • For the checksum approach to work, the one or more attributes used in computing the checksum must be updated. The algorithm used to compute the checksum must reliably reflect differences in input. If many different inputs product the same checksum, the algorithm will not reliably detect partial updates. "Salting" the input with values like the objectGUID of the source computer and the date and time of the update is also helpful.
  • Object counts work best when used with new sets of objects, or in combination with consistency GUIDs (see the next section for more information). The application performing the update must either know a priori the number of objects that will be in the container when the update is completed. or use some other means of marking the container invalid while the update proceeds (for example, setting the count to zero). After completing the update the source application marks the container with the count of objects contained.


Consistency GUIDs

Consistency GUIDs are a detection strategy that allows an application to detect partial updates. A Consistency GUID (Globally Unique IDentifier) is applied to each object in a related set. In implementation, a source application generates a new GUID and applies it to each object it updates in the set of related objects. It then applies the new GUID to the rest of the objects in the set, and finishes by applying the new GUID to the "master" object. Typically the "master" object will be a container that is the parent of the other objects in the set.

Some important considerations:

  • Consistency GUIDs combined with object counts or checksums are more effective than consistency GUIDs alone, because the application reading the objects may not know how many objects with the GUID should be present.
  • Applications must generate their own GUIDs (a Win32 API, UuidCreate, provides this function), and not use the system-generated GUIDs found in an object's objectGUID attribute. This is because a consistency GUID needs to change each time the set of objects is updated. Object identity GUIDs found in objectGUID never change after the object has been created.
  • Consistency GUIDs assume that no object is shared among sets, so each set can have a unique consistency GUID.


Versioning and Fallback Strategies

When an application detects a partial update using one of the preceding techniques, or reads a set of objects whose effective date has not yet been reached, the application must deal with the situation gracefully. For some applications the graceful response is to "fall back" to a previous version of the objects in question. Active Directory does not provide a versioning facility — applications that want this capability must provide it themselves. Approaches to versioning include keeping the "last known good" values cached locally and storing multiple sets of objects in the directory, for example, in "old", "current", and "new" containers. Many other schemes are possible.

Implementations must take care to avoid unintended consequences: an earlier version of objects should be used only when a partial update is detected or the new objects are not yet "effective". Falling back because something in the application "doesn't work" might circumvent an administrator's intent. For example, two computers that formerly could communicate might find themselves unable to do so because of a change in IP Security policy. If this is intentional on the part of the administrator the affected systems should not fall back to the policy that allowed them to communicate, as this would be a security breach.


Controlling Access to Active Directory Objects

Every object in Active Directory is protected by Windows 2000 security. This security protection controls the operations that each security principal can perform in the directory. This chapter includes the following sections that discuss how a directory-enabled application can cope with and take advantage of the access control features of Active Directory.

  • An overview of access control in Active Directory.
  • How access control affects read operations, write operation, object creation and deletion.
  • Using the IADs and IDirectoryObject interfaces to work with an object's security descriptor.
  • Modifying the access permissions on an object.
  • How security descriptors are set on new objects.
  • Creating a security descriptor.
  • Using inheritance of access permissions to enable administrative access to an entire subtree of the directory.
  • Creating, modifying, and reading the default security descriptor for an object class.
  • Creating, setting, and checking extended rights for operations that go beyond those covered by the predefined rights.


How Access Control Works in Active Directory

Access control for Active Directory objects is based on Windows NT/Windows 2000 access-control model. For a detailed description of this model and its components such as security descriptors, access tokens, SIDs, ACLs, ACEs, see Access Control Model.

The basic outline of this model is as follows:

  • Security descriptor. Each directory object has its own security descriptor containing security information that protects the object. Among other things, the security descriptor can contain a discretionary access-control list (DACL). A DACL contains a list of access-control entries (ACEs). Each ACE allows or denies a set of access rights to a user or group. The access rights correspond to the operations, such as reading and writing properties, that can be performed on the object.
  • Security context. When you try to access a directory object, your application specifies the credentials of the security principal who is making the access attempt. Once authenticated, these credentials determine your application's security context, which includes the group memberships and privileges associated with the security principal. See Security Contexts and Active Directory.
  • Access check. The system grants access to an object only if the object's security descriptor grants the necessary access rights to the security principal attempting the operation (or to groups to which the security principal belongs).


Controlling Access to Objects and Their Properties

If your application creates objects in the directory, you probably want to control who can access those objects. To do this, you need to work with the object's security descriptor, and more specifically, with the DACL and its list of ACEs.

When an object is created, it receives a security descriptor. For a description of the rules that the system uses to create the DACL for a new object, see How Security Descriptors are Set on New Objects. These rules reveal that there are several things you can do to control the ACEs an object receives at creation time.

  • You can create a new security descriptor and attach it to the object at creation time. See Creating a Security Descriptor.
  • An object can inherit ACEs from its parent container. You can apply inheritable ACEs at any point in the directory hierarchy such that the ACEs are inherited by objects down the tree. See Inheritance and Delegation of Administration.
  • Every object class definition in the schema includes a default security descriptor which can have a default DACL. If you have the necessary access rights, you can specify the ACEs in the default DACL in the schema. See Default Security Descriptor.

In addition, you can modify the DACL of an existing object.

  • You can replace the DACL with a new one
  • You can read the existing DACL, modify it, and apply the modified DACL. See Setting Access Rights on an Object.

What can you do with these ACEs? The following list enumerates the most important capabilities of ACEs in Active Directory.

  • Control who can perform specified operations on an object
  • Control who has access to a specific property or set of properties of an object
  • Control who can create child objects in a container, including who can create a specific type of child object.
  • Define private access rights (extended rights) for an object type and control who can perform the operations protected by the private rights
  • Apply an ACE to a container object at the root of a directory subtree, such that the protections can be inherited automatically by all child objects down the tree.
  • Apply an ACE that is inherited automatically by a specific type of child object in a subtree
  • Create ACEs that grant rights to a security group, rather than to a single user
  • Apply ACEs to Group Policy Objects to control the accounts and computers affected by the policy.


Access Rights for Active Directory Objects

All directory objects use the same set of predefined access rights that correspond to the common directory operations. These rights are described in the ADS_RIGHTS_ENUM enumeration type.

In addition, you can define private access rights (extended rights) for operations that go beyond those covered by the predefined rights. See Extended Rights.


Security Contexts and Active Directory

When your application binds to Active Directory, it does so in the security context of a security principal, which can be a human user or an entity such as a computer or a Win32 service. The security context is the user account information that the system uses to enforce security when a thread tries to access a securable object. This information includes such things as the user's security identifier (SID), group memberships, and privileges.

A user establishes a security context by presenting credentials for authentication. If the credentials are authenticated, the system produces an access token that identifies the group memberships and privileges associated with the user's account. The system checks your access token whenever you try to access a directory object. It compares the information in your access token to the accounts and groups allowed or denied access by the object's security descriptor.

You can use the following methods to control the security context with which you bind to Active Directory.

  • Bind using the ADS_SECURE_AUTHENTICATION option with the ADsOpenObject function or IADsOpenDSObject::OpenDSObject method and explicitly specify a username and password. The system authenticates these credentials and generates an access token that it uses for access checks for the duration of that binding. For more information, see Authentication.
  • Bind using the ADS_SECURE_AUTHENTICATION option but without specifying credentials. In this case, the system uses the primary security context of your application, that is, the security context of the user who started your application. In the case of a Win32-based service, this is the security context of the service account or the LocalSystem account.
  • Bind using ADsOpenObject or IADsOpenDSObject::OpenDSObject with the ADS_NO_AUTHENTICATION option. This method binds without authentication and results in "Everyone" as the security context. Only the LDAP provider supports this option.
  • Impersonate a user and then bind with ADS_SECURE_AUTHENTICATION but without specifying credentials. In this case, the system uses the security context of the client being impersonated. For more information, see Client Impersonation.

If possible, you should bind without specifying credentials. In other words, use the security context of the logged-on user or the impersonated client. This enables you to avoid caching credentials. If you must use alternate user credentials, prompt for the credentials, bind with them, but do not cache them. To use the same security context in multiple bind operations, you can specify the user name and password for the first bind operation and then specify only the user name to make subsequent binds. For details on using this technique, see Authentication.

Some security contexts are more powerful than others. For example, the LocalSystem account on a domain controller has complete access to Active Directory, whereas a typical user has only limited access to a some of the objects in the directory. In general, your application should not run in a powerful security context, such as LocalSystem, when a less powerful security context is sufficient to perform the operations you need to perform. This means that you may want to divide your application into separate components, each of which runs in a security context appropriate to the Active Directory operations you need to perform. For example, your application's setup could be broken up as follows:

  • Perform schema changes and extensions in the context of a user who is a member of the Schema Admins group.
  • Perform configuration container changes in the context of a user who is a member of the Enterprise Admins group.
  • Perform domain container changes in the context of a user who is a member of the Domain Admins group.


How Security Affects Active Directory Operations

Active Directory uses access control to grant or deny access to objects, properties, and operations based on the identity of the user making the access attempt. When your application binds to the directory, it binds with specific user credentials. Once authenticated, these credentials determine your application's security context. Regardless of whether the credentials are those of the logged-on user, a specified user, a service account, a computer account, or an unauthenticated user (Guest/Everyone), Active Directory checks the user's right to access an object before any operation is performed on that object. The user may or may not have access to a particular object, its children, its properties, or operations on that object, which means that your application needs to handle the potential errors caused by denied access.

The following topics discuss security contexts and the effects of access control on various operations.

  • Access Control and Read Operations
  • Access Control and Write Operations
  • Access Control and Object Creation
  • Access Control and Object Deletion


Access Control and Read Operations

Security is an implicit filter when performing searches, enumerating containers, or reading properties. If you don't have the necessary access rights, attempts to list objects or read properties can fail with the following error codes even thought the object or property exists:

E_ADS_INVALID_DOMAIN_OBJECT
E_ADS_PROPERTY_NOT_SUPPORTED
E_ADS_PROPERTY_NOT_FOUND

One important case to note is that a caller with ADS_RIGHT_ACTRL_DS_LIST access to a container can enumerate the child objects in the container. But an attempt to access a child object can still fail with an error such as E_ADS_UNKNOWN_OBJECT if the caller does not have ADS_RIGHT_ACTRL_DS_LIST_OBJECT access to the child object.

The impact of security on read operations is not necessarily manifested as an error. For example, a search operation can succeed but the search results do not include objects or properties to which the caller does not have access.


Access Control and Write Operations

Property modifications fail if the caller does not have sufficient rights. For write operations that batch modifications to multiple properties, the entire operation fails if the caller does not have the necessary rights to a single one of the modified properties. For example, you can make multiple IADs::Put calls to set multiple properties on an object. However, when you call IADs::SetInfo to write the new data from the local cache to the directory, SetInfo will fail if the caller does not have write access to all the modified properties. Similarly, IDirectoryObject::SetObjectAttributes fails to set any properties if the caller does not have access to all the properties being set. So you should batch multiple modify operations only if you know that all modifications will succeed. To determine the attributes of a directory object that the caller has the ability to modify, read the object's allowedAttributesEffective attribute.

If the caller does not have sufficient rights to modify a property, the following return codes may be returned:

E_ADS_PROPERTY_NOT_SET
E_ADS_PROPERTY_NOT_MODIFIED


Access Control and Object Creation

Active Directory will fail to create a child object if the caller does not have the ADS_RIGHT_DS_CREATE_CHILD for that object type on the parent container. To determine the types of child objects that the caller can create in a directory object, read the object's allowedChildClassesEffective attribute.

When you use the IADsContainer::Create method to create a child object, the object is not made persistent until IADs::SetInfo is called on the new object. Between the Create and SetInfo calls, the creating thread can put values into any of the new object's properties. After the SetInfo call, the creating thread does not necessarily have the access rights to set the new object's properties. To ensure that the caller has these rights, specify an explicit security descriptor during creation. The DACL should have an ACE that gives the caller the necessary access rights on the object.

For more information on access control and object creation, see How Security Descriptors are Set on New Objects.


Access Control and Object Deletion

Deletions fail if the caller does not have ADS_RIGHT_DS_DELETE_CHILD for that object type on the parent container.

If the object to delete is a container with child objects, a caller with ADS_RIGHT_DS_DELETE_TREE access to the object can delete all child objects regardless of the protections on the child objects. Otherwise, the caller must have the ADS_RIGHT_DS_DELETE_CHILD right for all objects in the tree.


APIs for Working with Security Descriptors

Every directory object has an nTSecurityDescriptor property that contains the object's security descriptor. There are two main ways to read and manipulate a directory object's security descriptor.

  • You can use the IADs::Get method to retrieve the security descriptor as an ADSI COM object with an IADsSecurityDescriptor interface. You can then use the IADsSecurityDescriptor, IADsAccessControlList, and IADsAccessControlEntry interfaces to work with the security descriptor and its components (ACLs, ACEs, and so on). For sample code that retrieves an IADsSecurityDescriptor interface pointer for a specified object's security descriptor, see Using IADs to Get a Security Descriptor.
  • You can use the IDirectoryObject::GetObjectAttributes method to retrieve an object's security descriptor as a pointer to a self-relative Win32 security descriptor (PSECURITY_DESCRIPTOR). Then you can use this pointer with the Win32 access-control functions such as AccessCheck and BuildSecurityDescriptor that have a PSECURITY_DESCRIPTOR parameter. For sample code that uses IDirectoryObject to retrieve a security descriptor, see Using IDirectoryObject to Get a Security Descriptor.

The recommended technique, and the one used by most of the samples in this guide, is to use the IADs interfaces because they simplify handling security descriptors, ACLs, and ACEs. For VB programmers, the IADs interfaces are the only easy way to work with security descriptors.

The IDirectoryObject technique is useful primarily when you need a PSECURITY_DESCRIPTOR pointer. For example, the sample code in the Checking an Extended Right in an Object's ACL uses this method to retrieve a security descriptor to pass to the AccessCheckByTypeResultList function.


Using IADs to Get a Security Descriptor

The following code fragment contains a function that returns an IADsSecurityDescriptor interface pointer to the ADSI COM object that represents the specified object's security descriptor:

HRESULT GetSDFromIADs(
                IADs *pObject,
                IADsSecurityDescriptor **pSD
                )

{
VARIANT var;
HRESULT hr = E_FAIL;
//Set *pSD to NULL.
if (*pSD)
  *pSD = NULL;
VariantClear(&var);
//Get the nTSecurityDescriptor
LPOLESTR szAttribute = L"nTSecurityDescriptor";
hr = pObject->Get(szAttribute,&var);
if (SUCCEEDED(hr))
{
    //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
    if (var.vt==VT_DISPATCH)
    {
        //Use V_DISPATCH macro to get the IDispatch pointer from the VARIANT structure
        //and QI for IADsSecurityDescriptor ptr.
        hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)pSD);
        if (FAILED(hr))
        {
            if (*pSD)
                (*pSD)->Release();
        }
    }
    else
        hr = E_FAIL;
}
VariantClear(&var);
return hr;
}


Using IDirectoryObject to Get a Security Descriptor

The following code fragment contains a function that uses the IDirectoryObject interface to retrieve the security descriptor of the specified object, create a buffer, copy the security descriptor to the buffer, and return a PSECURITY_DESCRIPTOR pointer to the security descriptor in the buffer:

HRESULT GetSDFromIDirectoryObject(
                      IDirectoryObject *pObject,
                      PSECURITY_DESCRIPTOR *pSecurityDescriptor
                      )

{
HRESULT hr = E_FAIL;
PADS_ATTR_INFO pAttrInfo;
DWORD   dwReturn= 0;
LPWSTR   pAttrName= L"nTSecurityDescriptor";
PSECURITY_DESCRIPTOR pSD = NULL;
//Get the nTSecurityDescriptor
hr = pObject->GetObjectAttributes( &pAttrName,
                                      1,
                                      &pAttrInfo,
                                      &dwReturn );
if ( (SUCCEEDED(hr)) && (dwReturn>0) )
{
   for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
   {
       //Check the attribute name
       if ( _wcsicmp(pAttrInfo->pszAttrName,L"nTSecurityDescriptor") == 0 )
       {
           //Check the attribute type.
           if (pAttrInfo->dwADsType==ADSTYPE_NT_SECURITY_DESCRIPTOR)
           {
               //Get a pointer to the security descriptor.
               pSD = (PSECURITY_DESCRIPTOR)(pAttrInfo->pADsValues->SecurityDescriptor.lpValue);
               DWORD SDSize = (pAttrInfo->pADsValues->SecurityDescriptor.dwLength);
               //Allocate memory for the buffer and copy the security descriptor to the buffer.
               *pSecurityDescriptor = (PSECURITY_DESCRIPTOR)CoTaskMemAlloc(SDSize);
               if (*pSecurityDescriptor)
                  CopyMemory((PVOID)*pSecurityDescriptor, (PVOID)pSD, SDSize);
               else
                   hr = E_FAIL;
               //Caller must free the memory for pSecurityDescriptor.
           }
       }
   }
}
//Free memory used for the attributes retrieved.
FreeADsMem( pAttrInfo );

return hr;
}


Security Descriptor Components

Having used the IADs::Get method to retrieve an IADsSecurityDescriptor interface pointer, you can use the property methods of the IADsSecurityDescriptor interface to read or write the components of a directory object's security descriptor. For example, to get or set the object's DACL, use the DiscretionaryAcl method (VB) or the put_DiscretionaryAcl and get_DiscretionaryAcl methods (C++).

A security descriptor can store the following information:

  • A security identifier (SID) that identifies the owner of the object. The owner of an object has the implicit right to modify the DACL and owner information in the object's security descriptor.
  • A discretionary access-control list (DACL) that identifies the users and groups who can perform various operations on the object. A DACL contains a list of access-control entries (ACEs). Each ACE allows or denies a specified set of access rights to a specified user account, group account, or other trustee. See Retrieving an Object's DACL.
  • A system access-control list (SACL) that controls how the system audits attempts to access the object. Each ACE in a SACL specifies the types of access attempts that generate an audit log entry for a specified user account, group account, or other trustee. See Retrieving an Object's SACL.
  • A set of SECURITY_DESCRIPTOR_CONTROL control flags that qualify the meaning of a security descriptor or its components. For example, the SE_DACL_PROTECTED flag protects the security descriptor's DACL from inheriting ACEs from its parent.
  • A security identifier (SID) that identifies the primary group of the object. Active Directory does not use this component.

For sample code that reads and displays the information in an object's security descriptor and DACL, see Reading an Object's Security Descriptor.


Retrieving an Object's DACL

An object's security descriptor may contain a discretionary access-control list (DACL). A DACL contains zero or more access-control entries (ACEs) that identify the users and groups who can access the object. If a DACL is empty (that is, it contains zero ACEs), no access is explicitly granted, so access is implicitly denied. However, if an object's security descriptor does not have a DACL, the object is unprotected and everyone has complete access.

To retrieve an object's DACL, you must be the object's owner or have READ_CONTROL access to the object.

To get the DACL from an IADsSecurityDescriptor object, use the IADsSecurityDescriptor::get_DiscretionaryAcl method. This method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface and use the methods on that interface to access the individual ACEs in the ACL.

To enumerate the ACEs, use the IADsAccessControlList::get__NewEnum method. The method returns an IUnknown pointer. Call QueryInterface on that IUnknown pointer to get an IEnumVARIANT interface. Use the IEnumVARIANT::Next method to enumerate the ACEs in the ACL. Each ACE is returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsAccessControlEntry interface for the ACE. You can use the methods of the IADsAccessControlEntry interface to set or retrieve the components of an ACE.

For more information about DACLs and ACEs, see the following topics in the Platform SDK.

  • Access-Control Lists (ACLs)
  • Access-Control Entries (ACEs)


Retrieving an Object's SACL

An object's security descriptor may contain a system access-control list (SACL). A SACL contains zero or more access-control entries (ACEs) that specify the types of access attempts that generate an audit log entry.

To set or retrieve the SACL in an object's security descriptor, the SE_SECURITY_NAME privilege must be enabled in the access token of the requesting thread. The administrators group has this privilege by default, and it can be assigned to other users or groups. For more information, see SACL Access Right.

To get the SACL from an IADsSecurityDescriptor object, use the IADsSecurityDescriptor::get_SystemAcl method. This method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface, and use the methods on that interface to access the individual ACEs in the ACL.

To enumerate the ACEs, use the IADsAccessControlList::get__NewEnum method. This method returns an IUnknown pointer. Call QueryInterface on that IUnknown pointer to get an IEnumVARIANT interface. Use the IEnumVARIANT::Next method to enumerate the ACEs in the ACL. Each ACE is returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsAccessControlEntry interface to read the ACE.

For more information about SACLs, see the following topics in the Platform SDK.

  • Access-Control Lists (ACLs)
  • Audit Generation


Reading an Object's Security Descriptor

This example uses the IADs interfaces to enumerate the properties of a directory object's security descriptor, DACL, and the ACEs of the DACL.

The code uses the IADs::Get method to retrieve the nTSecurityDescriptor property of the directory object. This method returns a VARIANT containing an IDispatch pointer. The code calls QueryInterface on that IDispatch pointer to get an IADsSecurityDescriptor interface to the object's security descriptor.

Next the code uses IADsSecurityDescriptor methods to retrieve information from the security descriptor. Note that the information available through IADsSecurityDescriptor depends on the access rights of the caller. The get_DiscretionaryAcl and get_Owner methods fail if the caller does not have READ_CONTROL access to the object. Similarly, a call to the get_SystemAcl method would fail if the caller does not have the SE_SECURITY_NAME privilege enabled.

HRESULT ReadSecurityDescriptor(
              IADs *pObject
              )

{
HRESULT hr = E_FAIL;

VARIANT var,varACE;
VARIANT_BOOL boolVal;
IADsSecurityDescriptor *pSD = NULL;
IADsAccessControlList *pACL = NULL;
IEnumVARIANT *pEnum = NULL;
IADsAccessControlEntry *pACE = NULL;
IDispatch *pDisp = NULL;
LPUNKNOWN pUnk = NULL;

ULONG lFetch;
BSTR szObjectType = NULL;
BSTR szTrustee = NULL;
BSTR szProp = NULL;
LPOLESTR szBool = NULL;
long lRev,lControl;
long lAceType, lAccessMask, lTypeFlag;

if (pObject)
{
  VariantClear(&var);
  //Get the nTSecurityDescriptor
  LPOLESTR szAttribute = L"nTSecurityDescriptor";
  hr = pObject->Get(szAttribute,&var);
  if (SUCCEEDED(hr))
  {
    //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
    if (var.vt==VT_DISPATCH)
    {
      //Use V_DISPATCH macro to get the IDispatch pointer from the VARIANT structure
      //and QI for IADsSecurityDescriptor ptr.
      hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)&pSD);
      if (SUCCEEDED(hr))
      {
          wprintf(L"****SECURITY DESCRIPTOR PROPERTIES****\n");
          //Get properties using IADsSecurityDescriptor property methods.
          hr = pSD->get_Revision(&lRev);
          wprintf(L"Revision: %d\n",lRev);
          hr = pSD->get_Control(&lControl);
          //Print out the control mask bits
          printf("Control Mask: \n");
          SDParseControlMasks(lControl);
          hr = pSD->get_Owner(&szProp);
          wprintf(L"Owner: %s\n",szProp);
          //Free the string.
          if (szProp)
            SysFreeString(szProp);
          hr = pSD->get_OwnerDefaulted(&boolVal);
          szBool = boolVal ? L"YES" : L"NO";
          wprintf(L"Owner Defaulted: %s\n",szBool);
          hr = pSD->get_Group(&szProp);
          wprintf(L"Group: %s\n",szProp);
          //Free the string.
          if (szProp)
            SysFreeString(szProp);
          hr = pSD->get_GroupDefaulted(&boolVal);
          szBool = boolVal ? L"YES" : L"NO";
          wprintf(L"Group Defaulted: %s\n",szBool);
          hr = pSD->get_SaclDefaulted(&boolVal);
          szBool = boolVal ? L"YES" : L"NO";
          wprintf(L"System ACL Defaulted: %s\n",szBool);
          hr = pSD->get_DaclDefaulted(&boolVal);
          szBool = boolVal ? L"YES" : L"NO";
          wprintf(L"Discretionary ACL Defaulted: %s\n",szBool);
        //Get the DACL
        hr = pSD->get_DiscretionaryAcl(&pDisp);
        if (SUCCEEDED(hr))
        {
          //QI for IADsAccessControlList interface
          wprintf(L"****DACL PROPERTIES****\n");
          hr = pDisp->QueryInterface(IID_IADsAccessControlList,(void**)&pACL);
          if (SUCCEEDED(hr))
          {
            //Enumerate the ACEs in the ACL.
            hr = pACL->get__NewEnum( &pUnk );
            if (SUCCEEDED(hr))
            {
              hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
              if (SUCCEEDED(hr))
              {
                hr = pEnum->Next( 1, &varACE, &lFetch );
                //Loop to read all ACEs on the object.
                while( hr == S_OK )
                {
                  //Check if 1 item returned and returned item is an IDispatch pointer.
                  if ( (lFetch == 1) && (varACE.vt==VT_DISPATCH))
                  {
                    //QI for IADsAccessControlEntry to use to read the ACE.
                    hr = V_DISPATCH(&varACE)->QueryInterface(
                           IID_IADsAccessControlEntry, (void**)&pACE );
                    if (SUCCEEDED(hr))
                    {
                      wprintf(L"****ACE PROPERTIES****\n");
                      //Get the trustee (who the right applies to) and print it.
                      hr = pACE->get_Trustee(&szTrustee);
                      if (SUCCEEDED(hr))
                      {
                        printf("Trustee: %S\n", szTrustee);
                        //Free the string.
                        if (szTrustee)
                          SysFreeString(szTrustee);
                      }
                      //Get the AceMask
                      hr = pACE->get_AccessMask(&lAccessMask);
                      if (SUCCEEDED(hr))
                      {
                        printf("AccessMask: \n");
                        SDParseAccessMask(lAccessMask);
                      }
                      //Get the AceType
                      hr = pACE->get_AceType(&lAceType);
                      if (SUCCEEDED(hr))
                      {
                        if (lAceType == ADS_ACETYPE_ACCESS_ALLOWED_OBJECT)
                          printf("ACE Type: ADS_ACETYPE_ACCESS_ALLOWED_OBJECT\n");
                        else if (lAceType == ADS_ACETYPE_ACCESS_DENIED_OBJECT)
                          printf("ACE Type: ADS_ACETYPE_ACCESS_DENIED_OBJECT\n");
                        else if (lAceType == ADS_ACETYPE_ACCESS_ALLOWED)
                          printf("ACE Type: ADS_ACETYPE_ACCESS_ALLOWED\n");
                        else if (lAceType == ADS_ACETYPE_ACCESS_DENIED)
                          printf("ACE Type: ADS_ACETYPE_ACCESS_DENIED\n");
                        else
                          printf("ACE Type: UNKNOWN TYPE: %x\n", lAceType);
                      }
                      //Get the Flags
                      hr = pACE->get_Flags(&lTypeFlag);
                      if (SUCCEEDED(hr))
                      {
                        //Based on flag, get objecttype and/or inheritedobjecttype.
                        if (lTypeFlag & ADS_FLAG_OBJECT_TYPE_PRESENT)
                        {
                          printf("Flags: ADS_FLAG_OBJECT_TYPE_PRESENT\n");
                          //Get the object type GUID and print it.
                          hr = pACE->get_ObjectType(&szObjectType);
                          if (SUCCEEDED(hr))
                          {
                              printf("ObjectType: %S\n", szObjectType);
                          }
                          //Free the string.
                          if (szObjectType)
                             SysFreeString(szObjectType);
                        }
                        if (lTypeFlag & ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT)
                        {
                          printf("Flags: ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT\n");
                          //Get the object type GUID and print it.
                          hr = pACE->get_InheritedObjectType(&szObjectType);
                          if (SUCCEEDED(hr))
                          {
                              printf("InheritedObjectType: %S\n", szObjectType);
                          }
                          //Free the string.
                          if (szObjectType)
                             SysFreeString(szObjectType);
                        }
                      }
                    }
                    //Clean up the enumerated ACE item.
                    if (pACE)
                      pACE->Release();
                  }
                  //Clean up the VARIANT for the ACE item.
                  VariantClear(&varACE);
                  //Get the next ACE
                  hr = pEnum->Next( 1, &varACE, &lFetch );
                };//End of While loop
              }
              //Clean up
              if (pEnum)
                 pEnum->Release();
            }
            if (pUnk)
               pUnk->Release();
          }
          if (pACL)
             pACL->Release();
        }
        if (pDisp)
           pDisp->Release();
      }
      if (pSD)
         pSD->Release();
    }
  }
  VariantClear(&var);
}
return hr;
}

//Function to print Control flags.
int SDParseControlMasks(
            long lCtrl
            )
{
  int iReturn = TRUE;

if (lCtrl & SE_OWNER_DEFAULTED)
  printf("  SE_OWNER_DEFAULTED\n");

if (lCtrl & SE_GROUP_DEFAULTED)
  printf("  SE_GROUP_DEFAULTED\n");

if (lCtrl & SE_DACL_PRESENT)
  printf("  SE_DACL_PRESENT\n");

if (lCtrl & SE_DACL_DEFAULTED)
  printf("  SE_DACL_DEFAULTED\n");

if (lCtrl & SE_SACL_PRESENT)
  printf("  SE_SACL_PRESENT\n");

if (lCtrl & SE_SACL_DEFAULTED)
  printf("  SE_SACL_DEFAULTED\n");

if (lCtrl & SE_DACL_AUTO_INHERIT_REQ)
  printf("  SE_DACL_AUTO_INHERIT_REQ\n");

if (lCtrl & SE_SACL_AUTO_INHERIT_REQ)
  printf("  SE_SACL_AUTO_INHERIT_REQ\n");

if (lCtrl & SE_DACL_AUTO_INHERITED)
  printf("  SE_DACL_AUTO_INHERITED\n");

if (lCtrl & SE_SACL_AUTO_INHERITED)
  printf("  SE_SACL_AUTO_INHERITED\n");

if (lCtrl & SE_DACL_PROTECTED)
  printf("  SE_DACL_PROTECTED\n");

if (lCtrl & SE_SACL_PROTECTED)
  printf("  SE_SACL_PROTECTED\n");

if (lCtrl & SE_SELF_RELATIVE)
  printf("  SE_OWNER_DEFAULTED\n");

return iReturn;

}

//Function to print AccessMask flags.
int SDParseAccessMask(
            long lCtrl
            )
{
  int iReturn = TRUE;

if (lCtrl & ADS_RIGHT_DELETE)
  printf("  ADS_RIGHT_DELETE\n");

if (lCtrl & ADS_RIGHT_READ_CONTROL)
  printf("  ADS_RIGHT_READ_CONTROL\n");

if (lCtrl & ADS_RIGHT_WRITE_DAC)
  printf("  ADS_RIGHT_WRITE_DAC\n");

if (lCtrl & ADS_RIGHT_WRITE_OWNER)
  printf("  ADS_RIGHT_WRITE_OWNER\n");

if (lCtrl & ADS_RIGHT_GENERIC_READ)
  printf("  ADS_RIGHT_GENERIC_READ\n");

if (lCtrl & ADS_RIGHT_GENERIC_WRITE)
  printf("  ADS_RIGHT_GENERIC_WRITE\n");

if (lCtrl & ADS_RIGHT_GENERIC_EXECUTE)
  printf("  ADS_RIGHT_GENERIC_EXECUTE\n");

if (lCtrl & ADS_RIGHT_GENERIC_ALL)
  printf("  ADS_RIGHT_GENERIC_ALL\n");

if (lCtrl & ADS_RIGHT_DS_CREATE_CHILD)
  printf("  ADS_RIGHT_DS_CREATE_CHILD\n");

if (lCtrl & ADS_RIGHT_DS_DELETE_CHILD)
  printf("  ADS_RIGHT_DS_DELETE_CHILD\n");

if (lCtrl & ADS_RIGHT_ACTRL_DS_LIST)
  printf("  ADS_RIGHT_ACTRL_DS_LIST\n");

if (lCtrl & ADS_RIGHT_DS_SELF)
  printf("  ADS_RIGHT_DS_SELF\n");

if (lCtrl & ADS_RIGHT_DS_READ_PROP)
  printf("  ADS_RIGHT_DS_READ_PROP\n");

if (lCtrl & ADS_RIGHT_DS_WRITE_PROP)
  printf("  ADS_RIGHT_DS_WRITE_PROP\n");

if (lCtrl & ADS_RIGHT_DS_DELETE_TREE)
  printf("  ADS_RIGHT_DS_DELETE_TREE\n");

if (lCtrl & ADS_RIGHT_DS_LIST_OBJECT)
  printf("  ADS_RIGHT_DS_LIST_OBJECT\n");

if (lCtrl & ADS_RIGHT_DS_CONTROL_ACCESS)
  printf("  ADS_RIGHT_DS_CONTROL_ACCESS\n");

return iReturn;

}


Setting Access Rights on an Object

When you are using the ADSI COM objects IADsSecurityDescriptor (security descriptor), IADsAccessControlList (DACLs and SACLs), and IADsAccessControlEntry (ACE) to add an ACE to a ACL, you are making changes to the nTSecurityDescriptor property of the specified object in the property cache. This means put methods on the objects that contain the new ACE and the IADs::SetInfo method must be called in order to write the updated security descriptor to the directory from the property cache.

Use the following steps for creating an ACE for an access right and setting that ACE on the DACL of an object (see the code fragment that follows for an example):

  1. Get an IADs interface pointer to the object.
  2. Use the IADs::Get method to get the security descriptor of the object. The name of the property containing the security descriptor is nTSecurityDescriptor. The property will be returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsSecurityDescriptor interface to use the methods on that interface to access the security descriptor's ACL.
  3. Use the IADsSecurityDescriptor::get_DiscretionaryAcl method to get the DACL. The method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface to use the methods on that interface to access the individual ACEs in the ACL.
  4. Use CoCreateInstance to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  5. Set the properties of the ACE using the IADsAccessControlEntry methods:
    1. Use IADsAccessControlEntry::put_Trustee to set the trustee to whom this ACE applies. The trustee is a user, group, or other security principal. Your application should use the value from the appropriate property from the user or group object of the trustee to which you want to apply the ACE. The trustee is specified as a BSTR and can take the following forms:

      Domain account (the logon name used in a previous version of Windows NT®) of the form domain\useraccount where domain is the name of the Windows NT domain that contains the user and useraccount is the sAMAccountName property of the specified user. For example: arcadiabay\jsmith.

      Well-known security principal that represents special identities defined by the Windows NT security system, such as everyone, local system, principal self, authenticated user, creator owner, and so on. The objects representing the well-known security principals are stored in the WellKnown Security Principals container beneath the Configuration container. For example, anonymous logon.

      Built-in group that represent the built-in user groups defined by the Windows NT security system. It has the form BUILTIN\groupname where groupname is the name of the built-in user group. The objects representing the built-in groups are stored in the Builtin container beneath the domain container. For example, BUILTIN\Administrators.

      SID (string format) of the specified user, which is the objectSID property of the specified user. You can convert to string form using the ConvertSidToStringSid function in the Win32 Security API. For example: S-1-5-32-548.

      Distinguished Name of the specified user, which is the distinguishedName property of the specified user. For example, CN=J Smith,OU=Northwest,dc=arcadiabay,DC=Com.

    2. Use IADsAccessControlEntry::put_AccessMask to set the mask that specifies the access right. The ADS_RIGHTS_ENUM enumeration specifies the access rights you can set on a directory object.
    3. Use IADsAccessControlEntry::put_AceType to specify whether to allow or deny the access rights set by put_AccessMask. For standard rights, this can be ADS_ACETYPE_ACCESS_ALLOWED or ADS_ACETYPE_ACCESS_DENIED. For object-specific rights (rights that apply to a specific part of an object or to a specific type of object), use ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT. The ADS_ACETYPE_ENUM enumeration specifies the access types you can set on an ACE.
    4. Use IADsAccessControlEntry::put_AceFlags to specify whether other containers or objects beneath the specified object can inherit the ACE. The ADS_ACEFLAG_ENUM enumeration specifies the inheritance flags you can set on an ACE.
    5. Use IADsAccessControlEntry::put_Flags to specify whether the right applies to a specific part of the object, an inherited object type, or both.
    6. If Flags is set to ADS_FLAG_OBJECT_TYPE_PRESENT, call IADsAccessControlEntry::put_ObjectType to specify a string containing the GUID of the object class (for ADS_RIGHT_DS_CREATE_CHILD or ADS_RIGHT_DS_DELETE_CHILD), property, property set, or extended right that the ACE applies to.
    7. If Flags is set to ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT, use IADsAccessControlEntry::put_InheritedObjectType to specify a string containing the GUID of the inherited object class that the ACE applies to.
  6. Use the QueryInterface method on the IADsAccessControlEntry object to get an IDispatch pointer. The AddAce method requires an IDispatch interface pointer to the ACE.
  7. Use IADsAccessControlList::AddAce to add the new ACE to the DACL. Note that the order of the ACEs within the ACL can affect the evaluation of access to the object. The correct access to the object may require you to create an new ACL, add the ACEs from the existing ACL in the correct order to the new ACL, and then replace the existing ACL in the security descriptor with the new ACL. For more information, see How ACE Order in an ACL Determines Access.
  8. Use IADsSecurityDescriptor::put_DiscretionaryAcl to write the DACL containing the new ACE to the security descriptor.
  9. Use the IADs::Put method to write the security descriptor to the object's nTSecurityDescriptor property to the property cache.
  10. Use the IADs::SetInfo method to update the property on the object in the directory.

The following code fragment is a function that adds an ACE to the ACL of the specified object:

HRESULT SetRight(
                          IADs *pObject,
                          long lAccessMask,
                          long lAccessType,
                          long lAccessInheritFlags,
                          LPOLESTR szObjectGUID,
                          LPOLESTR szInheritedObjectGUID,
                          LPOLESTR szTrustee
                          )

{
VARIANT var;
HRESULT hr = E_FAIL;
IADsAccessControlList *pACL = NULL;
IADsSecurityDescriptor *pSD = NULL;
IDispatch *pDisp = NULL;
IADsAccessControlEntry *pACE = NULL;
IDispatch *pDispACE = NULL;
long lFlags = 0L;
//This sample takes the szTrustee in an expected naming format
//and assumes it is the name for the correct trustee.
//Your application should check the validity of the specified trustee.
if (!szTrustee)
    return E_INVALIDARG;
if (pObject)
{
    VariantClear(&var);
    //Get the nTSecurityDescriptor
    LPOLESTR szAttribute = L"nTSecurityDescriptor";
    hr = pObject->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
        if (var.vt==VT_DISPATCH)
        {
            //Use the V_DISPATCH macro to get the IDispatch pointer from VARIANT structure
            //and QI for IADsSecurityDescriptor ptr.
            hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)&pSD);
            if (SUCCEEDED(hr))
            {
                //Get the DACL
                hr = pSD->get_DiscretionaryAcl(&pDisp);
                if (SUCCEEDED(hr))
                {
                    //QI for IADsAccessControlList interface
                    hr = pDisp->QueryInterface(IID_IADsAccessControlList,(void**)&pACL);
                    if (SUCCEEDED(hr))
                    {
                        //Create the COM object for the new ACE.
                        hr  = CoCreateInstance(
                                                    CLSID_AccessControlEntry,
                                                    NULL,
                                                    CLSCTX_INPROC_SERVER,
                                                    IID_IADsAccessControlEntry,
                                                    (void **)&pACE
                                                  );
                        if (SUCCEEDED(hr))
                        {
                            //Set the properties of the new ACE.

                            //Set the mask that specifies the access right.
                            hr = pACE->put_AccessMask( lAccessMask );
                            //Set the trustee.
                            hr = pACE->put_Trustee( szTrustee );
                            //Set AceType.
                            hr = pACE->put_AceType( lAccessType );
                            //Set AceFlags to specify whether other objects can inherit the ACE from the specified object.
                            hr = pACE->put_AceFlags( lAccessInheritFlags );

                            //If an szObjectGUID is specified, add the ADS_FLAG_OBJECT_TYPE_PRESENT flag to the lFlags mask
                            //and set the ObjectType.
                            if (szObjectGUID)
                            {
                                lFlags |= ADS_FLAG_OBJECT_TYPE_PRESENT;
                                hr = pACE->put_ObjectType( szObjectGUID );
                            }
                            //If an szInheritedObjectGUID is specified, add ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT flag to the lFlags mask
                            //and set the InheritedObjectType.
                            if (szInheritedObjectGUID)
                            {
                                lFlags |= ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT;
                                hr = pACE->put_InheritedObjectType( szInheritedObjectGUID );
                            }
                            //Set flags if ObjectType or InheritedObjectType were set.
                            if (lFlags)
                            {
                                hr = pACE->put_Flags(lFlags);
                            }
                            //Add the ACE to the ACL.
                            //Need to QI for the IDispatch pointer to pass to the AddAce method.
                            hr = pACE->QueryInterface(IID_IDispatch,(void**)&pDispACE);
                            if (SUCCEEDED(hr))
                            {
                                hr = pACL->AddAce(pDispACE);
                                if (SUCCEEDED(hr))
                                {
                                    //Write the DACL
                                    hr = pSD->put_DiscretionaryAcl(pDisp);
                                    if (SUCCEEDED(hr))
                                    {
                                        //Write the ntSecurityDescriptor property to the property cache.
                                        hr = pObject->Put(szAttribute, var);
                                        if (SUCCEEDED(hr))
                                        {
                                            //Call SetInfo to update the property on the object in the directory.
                                            hr = pObject->SetInfo();
                                        }
                                    }
                                }
                            }
                            if (pDispACE)
                                pDispACE->Release();
                        }
                        if (pACE)
                            pACE->Release();
                    }
                    if (pACL)
                       pACL->Release();
                }
                if (pDisp)
                   pDisp->Release();
            }
            if (pSD)
               pSD->Release();
        }
        else
            hr = E_FAIL;
    }
}

VariantClear(&var);
return hr;
}


Setting Access Rights on the Entire Object

Certain permissions can only be set for the entire object, such as Delete, List Contents, and so on. Granular permissions such as the Read Property can also be set for entire object so that they apply to entire object.

To set permissions that apply to the entire object:

  • Set AceType to ADS_ACETYPE_ACCESS_ALLOWED or ADS_ACETYPE_ACCESS_DENIED
  • Set ObjectType and InheritedObjectType to NULL

C/C++

Use the following steps for creating an ACE for an access right on the entire object, and make sure you follow the steps for setting that ACE on the DACL of the object:

  1. Get the object, security descriptor, DACL, and a pointer to the IADsAccessControlList interface. For details, see Setting Access Rights on an Object.
  2. Use the CoCreateInstance method to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  3. Use the IADsAccessControlEntry::put_Trustee method to set the trustee to whom this ACE applies. For details on how to specify the trustee, see Setting Access Rights on an Object.
  4. Use the IADsAccessControlEntry::put_AccessMask method to set the mask that specifies the access right. The ADS_RIGHTS_ENUM enumeration specifies the access rights you can set on a directory object.
  5. Use the IADsAccessControlEntry::put_AceType method with one of the following flags: ADS_ACETYPE_ACCESS_ALLOWED or ADS_ACETYPE_ACCESS_DENIED.
  6. Use the IADsAccessControlEntry::put_AceFlags method to specify whether other containers or objects beneath the specified object can inherit the ACE. The ADS_ACEFLAG_ENUM enumeration specifies the inheritance flags you can set on an ACE.
  7. Use the IADsAccessControlEntry::put_Flags method to specify whether the right applies to a specific part of the object, inherited object type, or both.
  8. Use the IADsAccessControlEntry::put_ObjectType method and specify NULL for the string.
  9. Use the IADsAccessControlEntry::put_InheritedObjectType method and specify NULL for the string.
  10. Follow the steps for setting that ACE on the DACL of the object. For details, see Setting Access Rights on an Object.

The following code fragment contains a function that creates an ACE that assigns read access to all properties of the object to the specified trustee:

//Create an ACE that assigns read property rights to all properties on the object.
//This ACE is not inherited, that is, it applies only to the current object.
HRESULT CreateAceEffectiveReadAllProperties(
                           LPOLESTR szTrustee,
                           IDispatch **ppDispACE)
{

HRESULT hr = E_FAIL;
IADsAccessControlEntry *pACE = NULL;
//Create the COM object for the new ACE.
hr  = CoCreateInstance(
                            CLSID_AccessControlEntry,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IADsAccessControlEntry,
                            (void **)&pACE
                          );
if (SUCCEEDED(hr))
{
    //Set the properties of the new ACE.
    //Set the access mask containing the rights to assign.
    //This function assigns read property rights.
    hr = pACE->put_AccessMask(ADS_RIGHT_DS_READ_PROP);
    //Set the trustee.
    hr = pACE->put_Trustee( szTrustee );
    //Set AceType
    hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_ALLOWED );
    //For this function, set AceFlags so that ACE is not inherited by child objects.
    //You can set AceFlags to 0 or let it default to 0 by not calling put_AceFlags.
    hr = pACE->put_AceFlags(0);
    //For this function, set ObjectType to NULL because the right applies to all properties
    //and set Flags to 0. You can also not call these two methods and let them default to NULL.
    hr = pACE->put_ObjectType( NULL );
    hr = pACE->put_Flags(0);
    //Is not inherited, so set object type to NULL or let it default to NULL by not calling the method.
    hr = pACE->put_InheritedObjectType( NULL );
    //Need to QI for IDispatch pointer to pass to the AddAce method.
    hr = pACE->QueryInterface(IID_IDispatch,(void**)ppDispACE);
}

return hr;
}


Setting Permissions to a Specific Property

Permissions can be set to apply to a specific property of an object.

To set permissions that apply to a specific property of an object:

  • Set the AccessMask to ADS_RIGHT_DS_READ_PROP and/or ADS_RIGHT_DS_WRITE_PROP.
  • Set AceType to ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  • Set ObjectType to the schemaIDGUID of the property. This is the schemaIDGUID of the attributeSchema object that defines the property in the schema.
  • Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT.

C/C++

Use the following steps for creating an ACE to grant access right for a specific property on the object and make sure you follow the steps for setting that ACE on the DACL of the object:

  1. Get the object, security descriptor, DACL, and a pointer to the IADsAccessControlList interface. For details, see Setting Access Rights on an Object.
  2. Use the CoCreateInstance function to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  3. Use the IADsAccessControlEntry::put_Trustee method to set the trustee to whom this ACE applies. For details on how to specify the trustee, see Setting Access Rights on an Object.
  4. Use the IADsAccessControlEntry::put_AccessMask method with any combination of the following rights flags: ADS_RIGHT_DS_READ_PROP or ADS_RIGHT_DS_WRITE_PROP.
  5. Use the IADsAccessControlEntry::put_AceType method with one of the following flags: ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  6. Use the IADsAccessControlEntry::put_Flags method with the ADS_FLAG_OBJECT_TYPE_PRESENT flag. If this ACE should be inherited, set both the ADS_FLAG_OBJECT_TYPE_PRESENT and ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT flags.
  7. Use the IADsAccessControlEntry::put_ObjectType method to specify schemaIDGUID of the property. This is the schemaIDGUID of the attributeSchema object that defines the property in the schema. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
  8. Use the IADsAccessControlEntry::put_AceFlags method to specify whether other objects beneath the specified object can inherit the ACE. The ADS_ACEFLAG_ENUM enumeration specifies the inheritance flags you can set on an ACE.
  9. If Flags contains ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT, use IADsAccessControlEntry::put_InheritedObjectType to specify the GUID of the schemaIDGUID property of the classSchema object that identifies the type of object. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
  10. Follow the steps for setting that ACE on the DACL of the object. For details, see Setting Access Rights on an Object.

The following code fragment contains a function that creates an ACE that assigns read/write access to the telephoneNumber property of user objects to the specified trustee:

//Create an ACE that assigns change (Read/Write) property rights
//to the telephoneNumber property of user objects.
//The ACE is inherited only by user objects beneath the current object;
//therefore, it is not an effective right on the current object.

HRESULT CreateAceChangeTelephoneNumberPropertyOfUsers(
                           LPOLESTR szTrustee,
                           BOOL bAllowed,
                           IDispatch **ppDispACE
                           )
{

HRESULT hr = E_FAIL;
IADsAccessControlEntry *pACE = NULL;
//Create the COM object for the new ACE.
hr  = CoCreateInstance(
                            CLSID_AccessControlEntry,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IADsAccessControlEntry,
                            (void **)&pACE
                          );
if (SUCCEEDED(hr))
{
    //Set the properties of the new ACE.
    //Set the access mask containing the rights to assign.
    //This function assigns ADS_RIGHT_DS_READ_PROP|ADS_RIGHT_DS_WRITE_PROP to control change.
    hr = pACE->put_AccessMask(ADS_RIGHT_DS_READ_PROP|ADS_RIGHT_DS_WRITE_PROP);
    //Set the trustee.
    hr = pACE->put_Trustee( szTrustee );
    //AceType must be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
    if (bAllowed)
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_ALLOWED_OBJECT );
    else
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_DENIED_OBJECT );
    //Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT|ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT
    //so that the right applies only to a specific property of the specified object class.
    hr = pACE->put_Flags(ADS_FLAG_OBJECT_TYPE_PRESENT|ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT);
    //Set ObjectType to the schemaIDGUID of telephoneNumber.
    hr = pACE->put_ObjectType( L"{bf967a49-0de6-11d0-a285-00aa003049e2}" );
    //For this function, set AceFlags so that ACE is inherited by child objects
    //but not effective on the current object.
    //Set AceFlags to ADS_ACEFLAG_INHERIT_ACE and ADS_ACEFLAG_INHERIT_ONLY_ACE.
    hr = pACE->put_AceFlags(ADS_ACEFLAG_INHERIT_ACE|ADS_ACEFLAG_INHERIT_ONLY_ACE);
    //Set InheritedObjectType to schemaIDGUID of User class.
    hr = pACE->put_InheritedObjectType( L"{bf967aba-0de6-11d0-a285-00aa003049e2}" );
    //Need to QI for IDispatch pointer to pass to the AddAce method.
    hr = pACE->QueryInterface(IID_IDispatch,(void**)ppDispACE);
}

return hr;
}


Setting Permissions on a Group of Properties

Permissions can also be applied to a group of properties. A property group is identified by the GUID in the rightsGUID property of a controlAccessRight object. This GUID is set in the attributeSecurityGUID property of the attributeSchema object of each property in the group.

To set permissions that apply to a group of properties in an object:

  • Set the AccessMask to ADS_RIGHT_DS_READ_PROP and/or ADS_RIGHT_DS_WRITE_PROP.

    Do not set the ADS_RIGHT_DS_CONTROL_ACCESS flag, which is used to specify an extended right only.

  • Set AceType to ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  • Set ObjectType to the GUID of the property group. This is the rightsGUID property of the controlAccessRights object that identifies the property group. This GUID is also set as the attributeSecurityGUID in the attributeSchema object of each property in the group.
  • Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT.

C/C++

Use the following steps for creating an ACE to grant access right for a property set on the entire object and make sure you follow the steps for setting that ACE on the DACL of the object:

  1. Get the object, security descriptor, DACL, and a pointer to the IADsAccessControlList interface. For details, see Setting Access Rights on an Object.
  2. Use the CoCreateInstance function to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  3. Use the IADsAccessControlEntry::put_Trustee method to set the trustee to whom this ACE applies. For details on how to specify the trustee, see Setting Access Rights on an Object.
  4. Use the IADsAccessControlEntry::put_AccessMask method with any combination of the following rights flags: ADS_RIGHT_DS_READ_PROP or ADS_RIGHT_DS_WRITE_PROP.
  5. Use the IADsAccessControlEntry::put_AceType method with one of the following flags: ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  6. Use the IADsAccessControlEntry::put_Flags method with the ADS_FLAG_OBJECT_TYPE_PRESENT flag. If this ACE should be inherited, set both the ADS_FLAG_OBJECT_TYPE_PRESENT and ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT flags.
  7. Use the IADsAccessControlEntry::put_ObjectType method to specify the GUID in the rightsGUID property of the controlAccessRights object that identifies the property group. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
  8. Use the IADsAccessControlEntry::put_AceFlags method to specify whether other objects beneath the specified object can inherit the ACE. The ADS_ACEFLAG_ENUM enumeration specifies the inheritance flags you can set on an ACE.
  9. If Flags contains ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT, use IADsAccessControlEntry::put_InheritedObjectType to specify the GUID of the schemaIDGUID property of the classSchema object that identifies the type of object. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
  10. Follow the steps for setting that ACE on the DACL of the object. For details, see Setting Access Rights on an Object.

The following code fragment contains a function that creates an ACE that assigns read/write access to the telephoneNumber property of user objects to the specified trustee:

//Create an ACE that assigns change (Read/Write) property rights
//to properties of the Personal Information property group in user objects.
//For this function, the ACE is inherited only;
//therefore, it is not an effective right on the current object.
HRESULT CreateAceChangePersonalInfoPropGroupOfUsers(
                           LPOLESTR szTrustee,
                           BOOL bAllowed,
                           IDispatch **ppDispACE
                           )
{

HRESULT hr = E_FAIL;
IADsAccessControlEntry *pACE = NULL;
//Create the COM object for the new ACE.
hr  = CoCreateInstance(
                            CLSID_AccessControlEntry,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IADsAccessControlEntry,
                            (void **)&pACE
                          );
if (SUCCEEDED(hr))
{
    //Set the properties of the new ACE.
    //Set the access mask containing the rights to assign.
    //This function assigns ADS_RIGHT_DS_READ_PROP|ADS_RIGHT_DS_WRITE_PROP to control change.
    hr = pACE->put_AccessMask(ADS_RIGHT_DS_READ_PROP|ADS_RIGHT_DS_WRITE_PROP);
    //Set the trustee.
    hr = pACE->put_Trustee( szTrustee );
    //AceType must be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
    if (bAllowed)
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_ALLOWED_OBJECT );
    else
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_DENIED_OBJECT );
    //Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT|ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT
    //so that the right applies only to a specific property of the specified object class.
    hr = pACE->put_Flags(ADS_FLAG_OBJECT_TYPE_PRESENT|ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT);
    //Set ObjectType to the rightsGUID of the personalInformation controlAccessRight object.
    hr = pACE->put_ObjectType( L"{77B5B886-944A-11d1-AEBD-0000F80367C1}" );
    //For this function, set AceFlags so that ACE is inherited by child objects
    //but not effective on the current object.
    //Set AceFlags to ADS_ACEFLAG_INHERIT_ACE and ADS_ACEFLAG_INHERIT_ONLY_ACE.
    hr = pACE->put_AceFlags(ADS_ACEFLAG_INHERIT_ACE|ADS_ACEFLAG_INHERIT_ONLY_ACE);
    //Set InheritedObjectType to schemaIDGUID of the user class.
    hr = pACE->put_InheritedObjectType( L"{bf967aba-0de6-11d0-a285-00aa003049e2}" );
    //Need to QI for the IDispatch pointer to pass to the AddAce method.
    hr = pACE->QueryInterface(IID_IDispatch,(void**)ppDispACE);
}

return hr;
}


Setting Permissions on Child Object Operations

Permissions (such as Create Child and Delete Child) can also be granted or denied for operations on all subobjects or subobjects that are of a specific class.

To set permissions for a specific subobject type:

  • Set AceType to ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  • Set ObjectType to the GUID for object class. This is the schemaIDGUID property of the classSchema object that defines the object class. If ObjectType is NULL, the ACE applies to subobjects of any class.
  • Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT.

The following code fragment contains a function that creates an ACE that assigns creation rights for user objects to the specified trustee:

//Create an ACE that assigns the right to create User objects
//beneath the current object.
//For this function, the ACE is inherited by all subobjects
//and is an effective right on the current object.
HRESULT CreateAceCreateUsers(
                           LPOLESTR szTrustee,
                           BOOL bAllowed,
                           IDispatch **ppDispACE
                           )
{

HRESULT hr = E_FAIL;
IADsAccessControlEntry *pACE = NULL;
//Create the COM object for the new ACE.
hr  = CoCreateInstance(
                            CLSID_AccessControlEntry,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IADsAccessControlEntry,
                            (void **)&pACE
                          );
if (SUCCEEDED(hr))
{
    //Set the properties of the new ACE.
    //Set the access mask containing the rights to assign.
    //This function assigns rights to create objects.
    hr = pACE->put_AccessMask(ADS_RIGHT_DS_CREATE_CHILD);
    //Set the trustee.
    hr = pACE->put_Trustee( szTrustee );
    //AceType must be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
    if (bAllowed)
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_ALLOWED_OBJECT );
    else
        hr = pACE->put_AceType( ADS_ACETYPE_ACCESS_DENIED_OBJECT );
    //Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT
    //so that the right applies to the creation of a specific object class
    //within the current object and all its subobjects.
    hr = pACE->put_Flags(ADS_FLAG_OBJECT_TYPE_PRESENT);
    //Set ObjectType to the schemaIDGUID of the user class so that the right
    //controls creation of user objects.
    hr = pACE->put_ObjectType( L"{bf967aba-0de6-11d0-a285-00aa003049e2}" );
    //For this function, set AceFlags so that ACE is inherited by child objects
    hr = pACE->put_AceFlags(ADS_ACEFLAG_INHERIT_ACE);
    //Set InheritedObjectType to NULL so that it is inherited by all subobjects.
    hr = pACE->put_InheritedObjectType(NULL);
    //Need to QI for the IDispatch pointer to pass to the AddAce method.
    hr = pACE->QueryInterface(IID_IDispatch,(void**)ppDispACE);
}

return hr;
}


How Security Descriptors are Set on New Objects

When you create a new object in the directory, you can explicitly create a security descriptor and then set that security descriptor as the object's nTSecurityDescriptor property. For a description of how to do this, see Creating a Security Descriptor.

Active Directory uses the following rules to set the DACL in the new object's security descriptor. Note that these rules distinguish between generic inheritable ACEs and object-specific inheritable ACEs. Generic inheritable ACEs can be inherited by all types of child objects. Object-specific inheritable ACEs can be inherited only by a specified type of child object.

  1. If you explicitly specify a security descriptor when you create the object, the system merges any any inheritable ACEs into the specified DACL unless the SE_DACL_PROTECTED bit is set in the security descriptor's control bits.
  2. If you do not specify a security descriptor and the DACL of the parent object has object-specific ACEs that are inheritable only by the type of object being created, the system builds the object's DACL from inheritable ACEs (including object-specific and generic inheritable ACEs). Note that in this case the new DACL does not get ACEs from the default DACL in the schema. If you want the child object to receive the default schema ACEs, read them from the schema and include them as inheritable ACEs in the container.
  3. If no DACL is specified and there are no inheritable object-specific ACEs for the type of object being created, the object's DACL is the default DACL from the schema for that object type. The system merges any generic inheritable ACEs into the default DACL.
  4. If the schema does not have a default DACL, the object's DACL is the default DACL from the primary or impersonation token of the creator.
  5. If there is no specified, inherited, or default DACL, the system creates the object with no DACL, which allows everyone full access to the object.

The system uses a similar algorithm to build a SACL for a directory service object.

The owner and primary group in the new object's security descriptor are set to the values you specify in the nTSecurityDescriptor property when you create the object. If you do not set these values, Active Directory uses the following rules to set them

Owner
The owner in a default security descriptor is set to the default owner SID from the primary or impersonation token of the creating process. For most users, the default owner SID is the same as the SID that identifies the user's account. Note that for users who are members of the built-in administrators group, the system automatically sets the default owner SID in the access token to the administrators group; therefore, objects created by a member of the administrators group are typically owned by the administrators group. To get or set the default owner in an access token, call the GetTokenInformation or SetTokenInformation function with the TOKEN_OWNER structure.
Primary Group
The primary group in a default security descriptor is set to the default primary group from the creator's primary or impersonation token. Note that primary group is not used in the context of Active Directory.

For more information on ACE inheritance, see Inheritance and Delegation of Administration.

For more information on the default security descriptors in the schema, see Default Security Descriptor.


Creating a Security Descriptor

Using ADSI, you can create a security descriptor and set it as a new object's nTSecurityDescriptor property or use it to replace an existing object's nTSecurityDescriptor property.

Use the following steps for creating a security descriptor for an object (see the code fragment that follows for an example):

  1. Use CoCreateInstance to create the ADSI COM object for the new security descriptor and get an IADsSecurityDescriptor interface pointer to that object. Note that the class ID is CLSID_SecurityDescriptor.
  2. Use the IADsSecurityDescriptor::put_Owner method to set the owner of the object. The trustee is a user, group, or other security principal. Your application should use the value from the appropriate property from the user or group object of the trustee to whom you want to apply the ACE. 3. Use the IADsSecurityDescriptor::put_Control method to control whether DACLs and SACLs are inherited by the object from its parent container.
  4. Use CoCreateInstance to create the ADSI COM object for the DACL for the new security descriptor and get an IADsAccessControlList interface pointer to that object. Note that the class ID is CLSID_AccessControlList.
  5. For each ACE to add to the DACL, use CoCreateInstance to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  6. For each ACE to add to the DACL, set the properties of the ACE using the property methods of the ACE's IADsAccessControlEntry object. For more information on the properties to set on an ACE, see Setting Access Rights on an Object.
  7. For each ACE to add to the DACL, use the QueryInterface method on the IADsAccessControlEntry object to get an IDispatch pointer. The AddAce method requires an IDispatch interface pointer to the ACE.
  8. For each ACE to add to the DACL, use IADsAccessControlList::AddAce to add the new ACE to the DACL. Note that the order of the ACEs within the ACL can affect the evaluation of access to the object. The correct access to the object may require you to create a new ACL, add the ACEs from the existing ACL in the correct order to the new ACL, and then replace the existing ACL in the security descriptor with the new ACL. For more information, see Order of ACEs in a DACL in the Platform SDK.
  9. Follow steps 4-8 to create the SACL for the new security descriptor.
  10. Use the IADsSecurityDescriptor::put_DiscretionaryAcl method to set the DACL.
  11. Use the IADsSecurityDescriptor::put_SystemAcl method to set the DACL.
  12. Get an IADs interface pointer to the object.
  13. Use the IADs::Put method to write the security descriptor to the object's nTSecurityDescriptor property to the property cache.
  14. Use the IADs::SetInfo method to update the property on the object in the directory.


Inheritance and Delegation of Administration

Active Directory supports inheritance of permissions down the object tree to allow administration to be done at higher levels in the tree. This allows administrators to set up inheritable permissions on objects near the root (such as domain and organizational units) and have those permissions flow down automatically to various objects in the tree.

Inheritance can be set on a per-ACE basis. You can specify the following flags in the AceFlags to control inheritance of the ACE:

ADS_ACEFLAG_INHERIT_ACE
This flag causes the ACE to be inherited down in the tree.
ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE
This flag causes the ACE to be inherited down only one level in the tree.
ADS_ACEFLAG_INHERIT_ONLY_ACE
This flag causes the ACE to be ignored on the object it is specified on and only be inherited down and be effective where it has been inherited.

In addition to setting inheritance, Active Directory supports object specific inheritance. This allows the inheritable ACEs to be inherited down the tree but be effective only on a specific type of object. This is extremely useful in delegating administration. For example, this can be used to set an object specific inheritable ACE at an organizational unit that allows a group to have full control on all user objects in the organizational unit but nothing else. Thereby, the management of users in that organizational unit gets delegated to the users in that group.

  • Security Groups and Delegation of Service Administration. Use Security groups to define and delegate administrative roles associated with your application server. For example, your service may be associated with a group MyService Admins. Users who are identified as the MyService administrators will be added to MyService Admins group. The setup program for MyService can set ACLs on the directory to allow MyService Admins sufficient permissions to read/write MyService-related attributes, create MyService specific objects, and so on.
  • Security Groups and Roles for Computers Running Your Service. Use security groups to define the set of computers that are granted access to your service's objects in the directory. For example, your service may be associated with a group MyService Servers. All computers running the MyService server are added to MyService Servers group and this group can then be given access to parts of the directory where MyService servers need to read/write information. The setup program for MyService can set ACLs on the directory to allow MyService Servers sufficient permissions to read/write MyService-related attributes, create MyService specific objects, and so on.


Access Control Inheritance

ACEs in an object's ACL can belong to one of two categories:

  • Effective ACL. ACEs in this category apply to the object itself.
  • Inherit ACL. ACEs in this category are inherited by objects created in the container.

Each ACE in the DACL can be in one or more categories. The categories for where an ACE belongs are determined by the inheritance control flags set in the ACE.

There are three inheritance control flags that can be set in the AceFlags property of an ACE:

ADS_ACEFLAG_INHERIT_ACE
This flag indicates that the ACE is part of the inherit ACL and that child objects will inherit the inheritance control flags of this ACE.
ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE
This flag indicates that the ACE is part of the inherit ACL, but that no inheritance control flags are propagated to direct child objects (direct descendants) and the ACE is effective on the direct child objects.
ADS_ACEFLAG_INHERIT_ONLY_ACE
This flag indicates that the ACE is not part of effective ACL.

If this flag is not set, then the ACE is part of the effective ACL. This flag is useful for setting permissions inheritable by subobjects, but do not affect accessibility of the container itself. For example, if an ACE is intended to be inherited by user objects in a organizational unit, there is a good chance that it should not be enforced for access to the organizational unit itself.

The ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE and ADS_ACEFLAG_INHERIT_ONLY_ACE flags are meaningful only if ADS_ACEFLAG_INHERIT_ACE is present. This is because the ADS_ACEFLAG_INHERIT_ACE flag adds inheritance behavior to an inheritable ACE but does not define the type of inheritance. The ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE and ADS_ACEFLAG_INHERIT_ONLY_ACE flags define a specific type of inheritance behavior.

Note that the system also sets the following flags based on the type and state of the ACE:

ADS_ACEFLAG_INHERITED_ACE
This flag indicates that the ACE was inherited.
ADS_ACEFLAG_VALID_INHERIT_FLAGS
This flag indicates that the inherit flags are valid.

The following table shows the effects of the different flag combinations for the AceFlags property of an ACE.

AceFlags | Effect on object containing the ACE | Effect on direct child objects | Effect on objects below direct children
No flags set. | Effective ACE: ACE applies to the object. | ACE is not inherited. | ACE is not inherited.
ADS_ACEFLAG_INHERIT_ACE | Effective ACE | ACE is inherited. ACE is an effective ACE. | ACE is inherited. ACE is an effective ACE.
ADS_ACEFLAG_INHERIT_ACE | ADS_ACEFLAG_INHERIT_ONLY_ACE | Not an Effective ACE: ACE does not apply to the object. | ACE is inherited. ACE is an effective ACE. | ACE is inherited. ACE is an effective ACE.
ADS_ACEFLAG_INHERIT_ACE | ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE | Effective ACE | ACE is inherited but without inheritance flags. ACE is an Effective ACE | ACE is not inherited.
ADS_ACEFLAG_INHERIT_ACE | ADS_ACEFLAG_INHERIT_ONLY_ACE | ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE | Not an Effective ACE. | ACE is inherited but without inheritance flags. ACE is an Effective ACE. | ACE is not inherited.


Setting Rights to Specific Types of Objects

To set object-specific inheritable ACEs:

  • Set AceType of ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  • Set ObjectType to NULL.
  • Set AceFlag to ADS_ACEFLAG_INHERIT_ACE.
  • Set InheritedObjectType to the type of object the ACE should be effective on.
  • Set Flags to ADS_FLAG_INHERITED OBJECT_TYPE_PRESENT.

Important You must set ADS_ACEFLAG_INHERIT_ACE to cause the ACE to be inherited. In addition, you must set ADS_ACEFLAG_INHERIT_ONLY_ACE if the object type this ACE applies to does not match the object type of the container where the ACE is specified. If this is not done, the ACE will also become effective on the container and can grant unexpected rights.

The following code fragment contains a function that creates an ACE that assigns rights that are inherited by the specified type of object but are not effective on the current object:

//Create an ACE that is inherited by child objects of the specified type
//but does not apply to the current object.
//This ACE is also propagated to all descendants of the current object.
HRESULT CreateAceNoEffectiveInheritObject(LPOLESTR szTrustee,
                           long lAccessRights,
                           long lAccessType,
                           LPOLESTR szObjectGUID,
                           LPOLESTR szInheritedObjectGUID,
                           IDispatch **ppDispACE)
{

HRESULT hr = E_FAIL;
IADsAccessControlEntry *pACE = NULL;
long lFlags = 0L;
//Create the COM object for the new ACE.
hr  = CoCreateInstance(
                            CLSID_AccessControlEntry,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IADsAccessControlEntry,
                            (void **)&pACE
                          );
if (SUCCEEDED(hr))
{
    //Set the properties of the new ACE.
    //Set the access mask containing the rights to assign.
    hr = pACE->put_AccessMask(lAccessRights);
    //Set the trustee.
    hr = pACE->put_Trustee( szTrustee );
    //Set AceType
    hr = pACE->put_AceType( lAccessType );
    //For this function, set AceFlags so that ACE is inherited by child objects
    //but not effective on the current object.
    //Set AceFlags to ADS_ACEFLAG_INHERIT_ACE and ADS_ACEFLAG_INHERIT_ONLY_ACE.
    hr = pACE->put_AceFlags(ADS_ACEFLAG_INHERIT_ACE|ADS_ACEFLAG_INHERIT_ONLY_ACE);
    //If an szObjectGUID is specified, add ADS_FLAG_OBJECT_TYPE_PRESENT flag to the lFlags mask
    //and set the ObjectType.
    if (szObjectGUID)
    {
        lFlags |= ADS_FLAG_OBJECT_TYPE_PRESENT;
        hr = pACE->put_ObjectType( szObjectGUID );
    }
    //If an szInheritedObjectGUID is specified, add ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT flag to the lFlags
mask
    //and set the InheritedObjectType.
    if (szInheritedObjectGUID)
    {
        lFlags |= ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT;
        hr = pACE->put_InheritedObjectType( szInheritedObjectGUID );
    }
    //Set flags if ObjectType or InheritedObjectType were set.
    if (lFlags)
    {
        hr = pACE->put_Flags(lFlags);
    }
    //Need to QI for IDispatch pointer to pass to the AddAce method.
    hr = pACE->QueryInterface(IID_IDispatch,(void**)ppDispACE);
}

return hr;
}


Setting Rights to Specific Properties of Specific Types of Objects

Property-specific permissions can be used in combination with object specific inheritance to provide the very powerful and granular delegation of administration. This can be used to accomplish things such as granting read/write telephone no. attributes on all user objects in a given organizational unit.

To set property-specific object-inheritable ACEs:

  • Set AceType to ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
  • Set ObjectType to the schema GUID of the attribute (for example, telephone #).
  • Set AceFlag to ADS_ACEFLAG_INHERIT_ACE.
  • Set InheritedObjectType to the type of object the ACE should be effective on (for example, user object).
  • Set Flags to ADS_FLAG_OBJECT_TYPE_PRESENT and ADS_FLAG_INHERITED OBJECT_TYPE_PRESENT.

Important You must set ADS_ACEFLAG_INHERIT_ACE to cause the ACE to be inherited. In addition, you must set ADS_ACEFLAG_INHERIT_ONLY_ACE if the object type this ACE applies to does not match the object type of the container where the ACE is specified. If this is not done, the ACE will also become effective on the container and can grant unexpected rights.

For sample code, see Setting Permissions to a Specific Property.


Protecting Objects from the Effects of Inherited Rights

As discussed in Inheritance and Delegation of Administration, ACEs can be set on a container object (such as an organizational unit, domainDNS, container, and so on) and propagated to child objects (based on the AceFlag property set on those ACEs).

If you have a highly secured object or an object whose ACEs you want to explicitly control (such as a private OU or a special user), you may want to prevent ACEs from being propagated to the object by its parent container (or its parent container's predecessors).

You can use the IADsSecurityDescriptor::put_Control method to control whether DACLs and SACLs are inherited by the object from its parent container.

The Control property can be used to protect an object from the effects of inherited ACEs. The following flags force access control to be set explicitly on the object and prevent a user from effectively modifying access control to the object by setting inheritable ACEs on the object's parent container (or its parent container's predecessors):

SE_DACL_PROTECTED | Prevents ACEs set on the DACL of the parent container (and any objects above the parent container in the directory hierarchy) from being applied to the object's DACL.
SE_SACL_PROTECTED | Prevents ACEs set on the SACL of the parent container (and any objects above the parent container in the directory hierarchy) from being applied to the object's SACL.

Note that the SE_DACL_PRESENT flag needs to be present to set SE_DACL_PROTECTED and SE_SACL_PRESENT needs to be present to set SE_SACL_PROTECTED.

The following code fragment is a function that sets/removes the SE_DACL_PROTECTED and SE_SACL_PROTECTED bits in the Control property of an object's security descriptor.

//This function sets/removes the SE_DACL_PROTECTED and SE_SACL_PROTECTED
//bits in the Control property.
//Valid values for lControl:
//0L means remove both SE_DACL_PROTECTED and SE_SACL_PROTECTED if they are set.
//SE_DACL_PROTECTED means add SE_DACL_PROTECTED and remove SE_SACL_PROTECTED.
//...and so on.
//Note that SE_DACL_PRESENT needs to be present to set SE_DACL_PROTECTED
//and SE_SACL_PRESENT needs to be present to set SE_SACL_PROTECTED.

HRESULT SetSDInheritProtect(
                  IADs *pObject,
                  long lControl
                  )

{
HRESULT hr = E_FAIL;

VARIANT var;
IADsSecurityDescriptor *pSD = NULL;

long lSetControl;
bool bChange = FALSE;

if (pObject)
{
    VariantClear(&var);
    //Get the nTSecurityDescriptor
    LPOLESTR szAttribute = L"nTSecurityDescriptor";
    hr = pObject->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
        if (var.vt==VT_DISPATCH)
        {
            //Use V_DISPATCH macro to get the IDispatch pointer from VARIANT structure
            //and QI for IADsSecurityDescriptor ptr.
            hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)&pSD);
            if (SUCCEEDED(hr))
            {
                //Get the Control property
                hr = pSD->get_Control(&lSetControl);
                //Parse the lControl and check for the bits in lSetControl

                //Check if SE_DACL_PROTECTED needs to be set.
                if (lControl & SE_DACL_PROTECTED)
                {
                    //Check if SE_DACL_PROTECTED is NOT set.
                    if (!(lSetControl & SE_DACL_PROTECTED))
                    {
                        lSetControl = lSetControl | SE_DACL_PROTECTED;
                        bChange = TRUE;
                    }
                }
                //SE_DACL_PROTECTED needs to be removed
                else
                {
                    if ((lSetControl &SE_DACL_PROTECTED)==SE_DACL_PROTECTED)
                    {
                        lSetControl=lSetControl-SE_DACL_PROTECTED;
                        bChange = TRUE;
                    }
                }

                //Check if SE_SACL_PROTECTED needs to be set.
                if (lControl & SE_SACL_PROTECTED)
                {
                    //Check if SE_SACL_PROTECTED is NOT set.
                    if (!(lSetControl & SE_SACL_PROTECTED))
                    {
                        lSetControl = lSetControl | SE_SACL_PROTECTED;
                        bChange = TRUE;
                    }
                }
                //SE_SACL_PROTECTED needs to be removed
                else
                {
                    if ((lSetControl &SE_SACL_PROTECTED)==SE_SACL_PROTECTED)
                    {
                        lSetControl=lSetControl-SE_SACL_PROTECTED;
                        bChange = TRUE;
                    }
                }

                //If there was change to the Control property,
                //write it to the Security Descriptor,
                //write the SD to object, and then call SetInfo
                //to write the object to the directory.
                if (bChange)
                {
                    hr = pSD->put_Control(lSetControl);
                    if (SUCCEEDED(hr))
                    {
                        hr = pObject->Put(szAttribute,var);
                        if (SUCCEEDED(hr))
                        {
                            hr = pObject->SetInfo();
                        }
                    }
                }
            }
            if (pSD)
               pSD->Release();
        }
    }
    VariantClear(&var);
}
return hr;
}


Default Security Descriptor

Active Directory also provides the capability of specifying default security for each type of object. This is specified in the defaultSecurityDescriptor attribute in the classSchema object definition in the Schema. This security descriptor is used to provide default protection on the object if there is no security descriptor specified during the creation of the object, and if there are no object-specific inherited permissions from the parent.

Note If the inherited permissions from the parent are all non-object specific, that is, they are inherited by all types of objects under the parent, defaults are included in the security descriptor applied to the object. Permission and auditing information specified in the default security descriptor is treated as explicit security on the object, that is, as if it were specified as part of object creation. Therefore, default permissions are placed in front of inherited permissions and override them as appropriate.

The defaultSecurityDescriptor is specified in a special string format defined in a security descriptor definition language (SDDL) syntax. This syntax is described in Microsoft® Software Developer's Kit documentation. There are two APIs provided to convert binary form of the security descriptor to string format and vice versa. These APIs are:

  • ConvertSecurityDescriptorToStringSecurityDescriptor
  • ConvertStringSecurityDescriptorToSecurityDescriptor


Reading the defaultSecurityDescriptor for an Object Class

Using ADSI, you can read the defaultSecurityDescriptor for an object class.

C/C++

If you are using ADSI, use the following steps for reading defaultSecurityDescriptor for an object class (see the following code fragment for an example):

  1. Get an IADs interface pointer to the schemaClass object for the object class.
  2. Use the IADs::Get method to get the default security descriptor of the object. The name of the property containing the security descriptor is defaultSecurityDescriptor. The property will be returned as a VARIANT containing a BSTR. The BSTR contains the default security descriptor in SDDL string format.
  4. Use the ConvertStringSecurityDescriptorToSecurityDescriptor function to convert the SDDL string form to a security descriptor.
  5. Use the Win32 Security APIs to read the parts of the security descriptor: GetSecurityDescriptorDacl, GetSecurityDescriptorSacl, GetSecurityDescriptorOwner, GetSecurityDescriptorControl.

The following code is a program that reads the defaultSecurityDescriptor for the specified object class:

#include <stdio.h>
#include <wchar.h>
#include <objbase.h>
#include <activeds.h>
#include <ACCCTRL.h>
//For security descriptor control flags
#include <winnt.h>
#define _WIN32_WINNT 0x0500
#include <Sddl.h>

HRESULT ReadDefaultSecurityDescriptor(
                          IADs *pObject
                          );

int SDParseControlMasks(
                        long lCtrl
                        );

int SDParseAccessMask(
                        long lCtrl
                        );

int main(int argc, char *argv[])
{
LPOLESTR szPath = new OLECHAR[MAX_PATH];
LPOLESTR pszBuffer  = new WCHAR[MAX_PATH];
HRESULT hr = S_OK;
IADs *pObject = NULL;
VARIANT var;

wprintf(L"This program displays the default security descriptor of an object class\n");
wprintf(L"Specify the object class:");
_getws(pszBuffer);
if (!pszBuffer)
    return TRUE;
wcscpy(szPath, L"LDAP://cn=");
wcscat(szPath, pszBuffer);
wcscat(szPath, L",");

//Intialize COM
CoInitialize(NULL);
//Get rootDSE and the schema container's DN.
//Bind to current user's domain using current user's security context.
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);

if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"schemaNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        wcscat(szPath,var.bstrVal);
        VariantClear(&var);
        if (pObject)
        {
           pObject->Release();
           pObject = NULL;
        }
        hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)&pObject);
        if (SUCCEEDED(hr))
        {
            wprintf(L"***********Read the default SD for the %s class***********\n", pszBuffer);
            hr = ReadDefaultSecurityDescriptor(
                                  pObject
                                  );
        }
    }
}

if (FAILED(hr))
   wprintf(L"Failed with the following HRESULT: %x\n",hr);

if (pObject)
   pObject->Release();

// Uninitialize COM
CoUninitialize();
return TRUE;

}

HRESULT ReadDefaultSecurityDescriptor(
                          IADs *pObject
                          )
{
HRESULT hr = E_FAIL;
PSECURITY_DESCRIPTOR pSDCNV = NULL;
BOOL bDaclPresent = FALSE;
BOOL bDaclDefaulted = FALSE;
PACL pDacl = NULL;
LPVOID pAce = NULL;
BYTE bAceType, bAceFlags;
PSID pSID = NULL;
DWORD dAccessMask;
OLECHAR szTrusteeName[MAX_PATH];
DWORD cbName = 0L;
OLECHAR szTrusteeDomainName[MAX_PATH];
DWORD cbReferencedDomainName = 0L;
SID_NAME_USE TrusteeType;
PACCESS_ALLOWED_ACE paace = NULL;
PACCESS_ALLOWED_OBJECT_ACE poace = NULL;
DWORD dFlags = 0L;
VARIANT var;
LPOLESTR szGUID = new WCHAR [39];

    hr = pObject->Get(L"defaultSecurityDescriptor",&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_BSTR.
        if (var.vt==VT_BSTR)
        {
            wprintf(L"Default SD: %s\n", var.bstrVal);
            if (ConvertStringSecurityDescriptorToSecurityDescriptor(var.bstrVal,
                                                                    SDDL_REVISION_1,
                                                                    &pSDCNV,
                                                                    NULL
                                                                    ))
            {
                //Read the security descriptor.
                //Get the DACL
                if (GetSecurityDescriptorDacl(
                    pSDCNV, // address of security descriptor
                    &bDaclPresent, // address of flag for presence of disc. ACL
                    &pDacl,  // address of pointer to ACL
                    &bDaclDefaulted // address of flag for default disc. ACL
                    ))
                {
                      printf("Ace count: %d\n",pDacl->AceCount);
                    for (WORD i = 0; i < pDacl->AceCount; i++ )
                    {
                        //Get the ACE
                        if (GetAce( pDacl, // pointer to access-control list
                                i, // index of ACE to retrieve
                                &pAce // pointer to pointer to ACE
                                ))
                        {
                            wprintf(L"**** ACE %d of %d ****\n", i+1, pDacl->AceCount);
                            bAceType = ((ACE_HEADER *)pAce)->AceType;
                            bAceFlags = ((ACE_HEADER *)pAce)->AceFlags;

                            switch (bAceType)
                            {
                            case ACCESS_ALLOWED_ACE_TYPE:
                                printf("ACE Type: ACCESS_ALLOWED_ACE_TYPE\n");
                                dAccessMask = ((ACCESS_ALLOWED_ACE *)pAce)->Mask;
                                paace = (PACCESS_ALLOWED_ACE)pAce;
                                pSID = (PSID)&(paace->SidStart);
                                break;
                            case ACCESS_DENIED_ACE_TYPE:
                                printf("ACE Type: ACCESS_DENIED_ACE_TYPE\n");
                                dAccessMask = ((ACCESS_DENIED_ACE *)pAce)->Mask;
                                paace = (PACCESS_ALLOWED_ACE)pAce;
                                pSID = (PSID)&(paace->SidStart);
                                break;
                            case ACCESS_ALLOWED_OBJECT_ACE_TYPE:
                                printf("ACE Type: ACCESS_ALLOWED_OBJECT_ACE_TYPE\n");
                                dAccessMask = ((ACCESS_ALLOWED_OBJECT_ACE *)pAce)->Mask;
                                poace = (PACCESS_ALLOWED_OBJECT_ACE)pAce;
                                //Check Flags to see
                                //if object type and/or inherited object type is set.
                                dFlags = poace->Flags;
                                if (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->ObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"ObjectType GUID: %s\n",szGUID);
                                }
                                //Print the inherited object type
                                //If both GUIDs are present, go to the member.
                                if ( (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->InheritedObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"Inherited ObjectType GUID: %s\n",szGUID);
                                }
                                //If only the inherited object type is present,
                                //we need to go to the ObjectType member.
                                if ( (!(dFlags & ACE_OBJECT_TYPE_PRESENT))
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->ObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"Inherited ObjectType GUID: %s\n",szGUID);
                                }

                                //Get the SID from the ACE.
                                if ( (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                   pSID = (PSID)&(poace->SidStart);
                                }
                                else if (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                {
                                   pSID = (PSID)&(poace->InheritedObjectType);
                                }
                                else if (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                {
                                   pSID = (PSID)&(poace->InheritedObjectType);
                                }
                                break;
                            case ACCESS_DENIED_OBJECT_ACE_TYPE:
                                printf("ACCESS_DENIED_OBJECT_ACE_TYPE\n");
                                dAccessMask = ((ACCESS_DENIED_OBJECT_ACE *)pAce)->Mask;
                                poace = (PACCESS_ALLOWED_OBJECT_ACE)pAce;
                                //Check Flags to see
                                //if object type and/or inherited object type is set.
                                dFlags = poace->Flags;
                                if (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->ObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"ObjectType GUID: %s\n",szGUID);
                                }
                                //Print the inherited object type
                                //If both GUIDs are present, go to the member.
                                if ( (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->InheritedObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"Inherited ObjectType GUID: %s\n",szGUID);
                                }
                                //If only the inherited object type is present,
                                //we need to go to the ObjectType member.
                                if ( (!(dFlags & ACE_OBJECT_TYPE_PRESENT))
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                    //Convert GUID to string.
                                    ::StringFromGUID2(poace->ObjectType, szGUID, 39);
                                    //Print the GUID
                                    wprintf(L"Inherited ObjectType GUID: %s\n",szGUID);
                                }

                                //Get the SID from the ACE.
                                if ( (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                     && (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                   )
                                {
                                   pSID = (PSID)&(poace->SidStart);
                                }
                                else if (dFlags & ACE_OBJECT_TYPE_PRESENT)
                                {
                                   pSID = (PSID)&(poace->InheritedObjectType);
                                }
                                else if (dFlags & ACE_INHERITED_OBJECT_TYPE_PRESENT)
                                {
                                   pSID = (PSID)&(poace->InheritedObjectType);
                                }
                                break;
                            default:
                                printf("Unknown ACE TYPE\n");
                                break;
                            }
                            cbName = sizeof(szTrusteeName);
                            cbReferencedDomainName = sizeof(szTrusteeDomainName);
                            ZeroMemory(szTrusteeName, cbName);
                            ZeroMemory(szTrusteeDomainName, cbReferencedDomainName);
                            //Look up the trustee name and domain.
                            if (LookupAccountSid(
                              NULL, // address of string for system name
                              pSID,             // address of security identifier
                              szTrusteeName,          // address of string for account name
                              &cbName,       // address of size account string
                              szTrusteeDomainName,
                                                    // address of string for referenced domain
                              &cbReferencedDomainName,
                                                    // address of size domain string
                              &TrusteeType   // address of structure for SID type
                              ))
                            {
                               if (wcslen(szTrusteeDomainName)==0)
                                 wprintf(L"Trustee: %s\n",szTrusteeName);
                               else
                                 wprintf(L"Trustee: %s\\%s\n", szTrusteeDomainName,szTrusteeName);
                            }
                            else
                            {
                                if (GetLastError() == ERROR_NONE_MAPPED)
                                {
                                    printf("Last Error: ERROR_NONE_MAPPED\n");
                                }
                                else
                                {
                                    printf("Last Error: %d\n",GetLastError());
                                }
                            }
                            printf("AccessMask: \n");
                            SDParseAccessMask(dAccessMask);
                            printf("Inheritance flags: %d\n",bAceFlags);
                        }
                    }
                }
            }
            if (pSDCNV)
                LocalFree(pSDCNV);
        }
    }
    VariantClear(&var);
return hr;
}

//Function to print Control flags.
int SDParseControlMasks(
                        long lCtrl
                        )
{
    int iReturn = TRUE;

if (lCtrl & SE_OWNER_DEFAULTED)
    printf("  SE_OWNER_DEFAULTED\n");

if (lCtrl & SE_GROUP_DEFAULTED)
    printf("  SE_GROUP_DEFAULTED\n");

if (lCtrl & SE_DACL_PRESENT)
    printf("  SE_DACL_PRESENT\n");

if (lCtrl & SE_DACL_DEFAULTED)
    printf("  SE_DACL_DEFAULTED\n");

if (lCtrl & SE_SACL_PRESENT)
    printf("  SE_SACL_PRESENT\n");

if (lCtrl & SE_SACL_DEFAULTED)
    printf("  SE_SACL_DEFAULTED\n");

if (lCtrl & SE_DACL_AUTO_INHERIT_REQ)
    printf("  SE_DACL_AUTO_INHERIT_REQ\n");

if (lCtrl & SE_SACL_AUTO_INHERIT_REQ)
    printf("  SE_SACL_AUTO_INHERIT_REQ\n");

if (lCtrl & SE_DACL_AUTO_INHERITED)
    printf("  SE_DACL_AUTO_INHERITED\n");

if (lCtrl & SE_SACL_AUTO_INHERITED)
    printf("  SE_SACL_AUTO_INHERITED\n");

if (lCtrl & SE_DACL_PROTECTED)
    printf("  SE_DACL_PROTECTED\n");

if (lCtrl & SE_SACL_PROTECTED)
    printf("  SE_SACL_PROTECTED\n");

if (lCtrl & SE_SELF_RELATIVE)
    printf("  SE_OWNER_DEFAULTED\n");

return iReturn;

}

//Function to print AccessMask flags.
int SDParseAccessMask(
                        long lCtrl
                        )
{
    int iReturn = TRUE;

if (lCtrl & ADS_RIGHT_DELETE)
    printf("  ADS_RIGHT_DELETE\n");

if (lCtrl & ADS_RIGHT_READ_CONTROL)
    printf("  ADS_RIGHT_READ_CONTROL\n");

if (lCtrl & ADS_RIGHT_WRITE_DAC)
    printf("  ADS_RIGHT_WRITE_DAC\n");

if (lCtrl & ADS_RIGHT_WRITE_OWNER)
    printf("  ADS_RIGHT_WRITE_OWNER\n");

if (lCtrl & ADS_RIGHT_GENERIC_READ)
    printf("  ADS_RIGHT_GENERIC_READ\n");

if (lCtrl & ADS_RIGHT_GENERIC_WRITE)
    printf("  ADS_RIGHT_GENERIC_WRITE\n");

if (lCtrl & ADS_RIGHT_GENERIC_EXECUTE)
    printf("  ADS_RIGHT_GENERIC_EXECUTE\n");

if (lCtrl & ADS_RIGHT_GENERIC_ALL)
    printf("  ADS_RIGHT_GENERIC_ALL\n");

if (lCtrl & ADS_RIGHT_DS_CREATE_CHILD)
    printf("  ADS_RIGHT_DS_CREATE_CHILD\n");

if (lCtrl & ADS_RIGHT_DS_DELETE_CHILD)
    printf("  ADS_RIGHT_DS_DELETE_CHILD\n");

if (lCtrl & ADS_RIGHT_ACTRL_DS_LIST)
    printf("  ADS_RIGHT_ACTRL_DS_LIST\n");

if (lCtrl & ADS_RIGHT_DS_SELF)
    printf("  ADS_RIGHT_DS_SELF\n");

if (lCtrl & ADS_RIGHT_DS_READ_PROP)
    printf("  ADS_RIGHT_DS_READ_PROP\n");

if (lCtrl & ADS_RIGHT_DS_WRITE_PROP)
    printf("  ADS_RIGHT_DS_WRITE_PROP\n");

if (lCtrl & ADS_RIGHT_DS_DELETE_TREE)
    printf("  ADS_RIGHT_DS_DELETE_TREE\n");

if (lCtrl & ADS_RIGHT_DS_LIST_OBJECT)
    printf("  ADS_RIGHT_DS_LIST_OBJECT\n");

if (lCtrl & ADS_RIGHT_DS_CONTROL_ACCESS)
    printf("  ADS_RIGHT_DS_CONTROL_ACCESS\n");

return iReturn;

}


Modifying the defaultSecurityDescriptor for an Object Class

The following sample code retrieves the default security descriptor for an object class, adds an ACE to the DACL, and then sets the modified security descriptor on the object class.

Note that schema modification is disabled by default on all Windows 2000 domain controllers. To enable schema modification at a particular DC, set a REG_DWORD value named "Schema Update Allowed" under the following registry key:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS\Parameters

Add this value if it does not already exist. Set this value to 1 to enable schema modification. If this value is zero, schema modification is disabled. The Schema Manager MMC snap-in provides a checkbox that sets or clears this registry key.

#include <wchar.h>
#include <objbase.h>
#include <activeds.h>
#include <ACLAPI.h>
#include <winnt.h>
#include <Sddl.h>

#define _WIN32_WINNT 0x0500

HRESULT ModifyDefaultSecurityDescriptor( IADs *pObject );

int main(int argc, char *argv[])
{
LPOLESTR szPath = new OLECHAR[MAX_PATH];
LPOLESTR pszBuffer  = new WCHAR[MAX_PATH];
HRESULT hr = S_OK;
IADs *pObject = NULL;
VARIANT var;

wprintf(L"This program modifies the default security descriptor of an object class\n");
wprintf(L"Specify the object class:");
_getws(pszBuffer);
if (!pszBuffer)
  return TRUE;
wcscpy(szPath, L"LDAP://cn=");
wcscat(szPath, pszBuffer);
wcscat(szPath, L",");

// Intialize COM

CoInitialize(NULL);

// Get rootDSE and the schema container's DN. Bind to the
// current user's domain using current user's security context.

hr = ADsOpenObject(L"LDAP://rootDSE",
            NULL,
            NULL,
            ADS_SECURE_AUTHENTICATION, // Use Secure Authentication
            IID_IADs,
            (void**)&pObject);

if (SUCCEEDED(hr)) {
  hr = pObject->Get(L"schemaNamingContext",&var);
  if (SUCCEEDED(hr)) {
    wcscat(szPath,var.bstrVal);
    VariantClear(&var);
    if (pObject) {
       pObject->Release();
       pObject = NULL;
    }
    hr = ADsOpenObject(szPath,
               NULL,
               NULL,
               ADS_SECURE_AUTHENTICATION, // Use Secure Authentication
               IID_IADs,
               (void**)&pObject);
    if (SUCCEEDED(hr)) {
      wprintf(L"Modify the default SD for the %s class\n", pszBuffer);
      hr = ModifyDefaultSecurityDescriptor( pObject );
    }
  }
}

if (FAILED(hr))
  wprintf(L"Failed with the following HRESULT: 0x%x\n", hr);

if (pObject)
  pObject->Release();

// Uninitialize COM.

CoUninitialize();
return TRUE;
}

HRESULT ModifyDefaultSecurityDescriptor(
                          IADs *pObject
                          )
{
HRESULT hr = E_FAIL;
VARIANT var;
PSECURITY_DESCRIPTOR pSDCNV = NULL;
SECURITY_DESCRIPTOR SD = {0};
DWORD dwSDSize = sizeof(SECURITY_DESCRIPTOR);
PSID pOwnerSID = NULL;
DWORD dwOwnerSIDSize = 0;
PSID pGroupSID = NULL;
DWORD dwGroupSIDSize = 0;
PACL pDACL = NULL;
DWORD dwDACLSize = 0;
PACL pSACL = NULL;
DWORD dwSACLSize = 0;
BOOL bDaclPresent = FALSE;
BOOL bDaclDefaulted = FALSE;
PACL pOldDACL, pNewDACL;
ULONG ulLen;
EXPLICIT_ACCESS ea;
DWORD dwRes;

// Get the default security descriptor. Type should be VT_BSTR.

hr = pObject->Get(L"defaultSecurityDescriptor", &var);
if (FAILED(hr) || var.vt!=VT_BSTR ) {
  wprintf(L"Error getting default SD: 0x%x\n", hr );
  goto Cleanup;
}

wprintf(L"Old Default SD: %s\n", var.bstrVal);

// Convert the security descriptor string to a security descriptor.

if ( ! ConvertStringSecurityDescriptorToSecurityDescriptor (
         var.bstrVal, SDDL_REVISION_1, &pSDCNV, NULL )) {
  wprintf(L"Error converting string security descriptor: %d\n",
         GetLastError() );
  goto Cleanup;
}

// Convert self-relative security descriptor to absolute.
// First get the required buffer sizes.

if (! MakeAbsoluteSD(pSDCNV, &SD, &dwSDSize,
           pDACL, &dwDACLSize,
           pSACL, &dwSACLSize,
           pOwnerSID, &dwOwnerSIDSize,
           pGroupSID, &dwGroupSIDSize) ) {

  // Allocate the buffers.

  pDACL = (PACL) GlobalAlloc(GPTR, dwDACLSize);
  pSACL = (PACL) GlobalAlloc(GPTR, dwSACLSize);
  pOwnerSID = (PACL) GlobalAlloc(GPTR, dwOwnerSIDSize);
  pGroupSID = (PACL) GlobalAlloc(GPTR, dwGroupSIDSize);
  if (! (pDACL && pSACL && pOwnerSID && pGroupSID) ) {
    wprintf(L"GlobalAlloc failed: %d\n", GetLastError() );
    goto Cleanup;
  }

  // Now do the conversion.

  if (! MakeAbsoluteSD(pSDCNV, &SD, &dwSDSize, pDACL, &dwDACLSize,
           pSACL, &dwSACLSize, pOwnerSID, &dwOwnerSIDSize,
           pGroupSID, &dwGroupSIDSize) ) {
    wprintf(L"MakeAbsoluteSD: %d\n", GetLastError() );
    goto Cleanup;
  }
}

// Get the DACL from the security descriptor.

if (! GetSecurityDescriptorDacl(&SD, &bDaclPresent,
                  &pOldDACL, &bDaclDefaulted) ) {
  wprintf(L"GetSecurityDescriptorDacl failed: %d\n", GetLastError() );
  goto Cleanup;
}

// Initialize an EXPLICIT_ACCESS structure for the new ACE.
// The ACE grants Everyone the right to read properties.

ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
ea.grfAccessPermissions = ADS_RIGHT_DS_READ_PROP;
ea.grfAccessMode = GRANT_ACCESS;
ea.grfInheritance= 0;
ea.Trustee.TrusteeForm = TRUSTEE_IS_NAME;
ea.Trustee.ptstrName = TEXT("Everyone");

// Create a new ACL that merges the new ACE into the existing DACL.

dwRes = SetEntriesInAcl(1, &ea, pOldDACL, &pNewDACL);
if (ERROR_SUCCESS != dwRes)  {
    wprintf(L"SetEntriesInAcl Error %u\n", dwRes );
    goto Cleanup;
}

// Put the modified DACL into the security descriptor.

if (! SetSecurityDescriptorDacl(&SD, TRUE, pNewDACL, FALSE) ) {
  wprintf(L"SetSecurityDescriptorOwner failed: %d\n",
            GetLastError() );
  goto Cleanup;
}

// Convert the security descriptor back to string format.

VariantClear(&var);
if ( ! ConvertSecurityDescriptorToStringSecurityDescriptor (
        &SD, SDDL_REVISION_1,
        GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION |
        DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION,
        &var.bstrVal, &ulLen )) {
  wprintf(L"Error converting security descriptor to string: %d\n",
        GetLastError() );
  goto Cleanup;
}

wprintf(L"New default SD: %s\n", var.bstrVal);
V_VT(&var) = VT_BSTR;

// Use Put and SetInfo to set the modified security descriptor.

hr = pObject->Put(L"defaultSecurityDescriptor", var);
if (FAILED(hr)) {
  wprintf(L"Error putting default SD: 0x%x\n", hr );
  goto Cleanup;
}

hr = pObject->SetInfo();
if (FAILED(hr)) {
  wprintf(L"Error setting default SD: 0x%x\n", hr );
  goto Cleanup;
}

Cleanup:

VariantClear(&var);
if (pSDCNV)
  LocalFree(pSDCNV);
if (pDACL)
  GlobalFree(pDACL);
if (pSACL)
  GlobalFree(pSACL);
if (pOwnerSID)
  GlobalFree(pOwnerSID);
if (pGroupSID)
  GlobalFree(pGroupSID);
if (pNewDACL)
  LocalFree(pNewDACL);

return hr;
}


Extended Rights

In Active Directory, access control (or access rights) means controlling who can perform operations on an object. The two major types of permissions are container operations and property-based access. However, there are other operations that have semantics that are not tied to specific properties. These operations may also require access control. For example, the user class can be granted a Send As right that can be used by Exchange, Outlook, or any other mail program, to determine whether a particular user can have another user send mail on their behalf. Access control on custom actions or operations are called control access rights or extended rights.


Extended Rights Container

The set of all extended rights are stored in the Extended Rights container within the Configuration container. Because the Configuration container is replicated across the entire forest, extended rights are propagated across all domains in a forest. Each extended right is stored as an controlAccessRight object in the Extended Rights container.

If you wanted to list all extended rights, you would simply enumerate all the controlAccessRight objects in the Extended Rights container.

The following code fragment binds to the Extended Rights container:

HRESULT hr = E_FAIL;
LPOLESTR szADsPath = new OLECHAR[MAX_PATH];
IADs *pObj = NULL;
IDirectoryObject *pDO2 = NULL;
VARIANT var;

hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)&pObj);

if (SUCCEEDED(hr))
{
    //Get the DN to the config container.
    hr = pObj->Get(L"configurationNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build ADsPath string to Extended-Rights container
        wcscpy(szADsPath,L"LDAP://cn=Extended-Rights,");
        wcscat(szADsPath,var.bstrVal);
        //Get an IDirectory Object pointer to the Extended Rights Container.
        hr = ADsOpenObject(szADsPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IDirectoryObject,
                         (void**)&pDO2);


controlAccessRight Objects

Each extended right is represented as a controlAccessRight object. The controlAccessRight object has four important properties:

cn
This property is a single-valued property that is the object's RDN. The cn is the name of the extended right in Active Directory.
appliesTo
This property is a multi-valued property that contains the list of object classes that the extended right applies to. In the list, an object class is represented by the schemaIDGUID property for its schemaClass object. The GUIDs are stored as strings of the form produced by the StringFromGUID2 function in the COM library — but without the starting and terminating curly braces ({ }). For example, the following GUID is the schemaIDGUID for the computer class:
bf967a86-0de6-11d0-a285-00aa003049e2
rightsGUID
Is the GUID used to represent the extended right within an ACE. The GUID is stored as a string of the form produced by the StringFromGUID2 function in the COM library — but without the starting and terminating curly braces.
displayName
Is the string used to display the extended right in the Access Control Settings dialog box and other places in the Active Directory Users and Computers snap-in.


Reading an Extended Right Set in an Object's ACL

Using ADSI, you read an extended right ACE just as you would any other ACE in an ACL. (Note that you can also use the Win32 security APIs to read ACLs on directory objects.) However, extended rights use the properties on the ACE in a manner that is specific to granting and denying extended rights:

  • AccessMask must contain the following flag:

    ADS_RIGHT_DS_CONTROL_ACCESS

  • Flags is ADS_FLAG_OBJECT_TYPE_PRESENT
  • ObjectType is the string form of the rightsGUID property of the extended right. The string format of the GUID is the same string format as the StringFromGUID2 COM Library function
  • AceType is either ADS_ACETYPE_ACCESS_ALLOWED_OBJECT to grant the trustee the extended right or ADS_ACETYPE_ACCESS_DENIED_OBJECT to deny the trustee the extended right
  • Trustee is the security principal (user, group, computer, and so on) to whom the ACE applies

C/C++

If you are using ADSI, use the following steps for reading ACEs for an extended right on an object (see the following code fragment for an example):

  1. Get an IADs interface pointer to the object.
  2. Use the IADs::Get method to get the security descriptor of the object. The name of the property containing the security descriptor is nTSecurityDescriptor. The property will be returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsSecurityDescriptor interface to use the methods on that interface to access the security descriptor's ACL.
  4. Use the IADsSecurityDescriptor::get_DiscretionaryAcl method to get the ACL. The method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface to use the methods on that interface to access the individual ACEs in the ACL.
  5. Use the IADsAccessControlList::get__NewEnum method to enumerate the ACEs. The method returns an IUnknown pointer. Call QueryInterface on that IUnknown pointer to get an IEnumVARIANT interface.
  6. Use the IEnumVARIANT::Next method to enumerate the ACEs in the ACL. The property will be returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsAccessControlEntry interface to read the ACE.
  7. Call the IADsAccessControlEntry::get_AccessMask method to get the AccessMask.
  8. Check the AccessMask value for the ADS_RIGHT_DS_CONTROL_ACCESS flag. If it has this flag, the ACE contains an extended right.
  9. Call IADsAccessControlEntry::get_Flags method to get the flag for object type.
  10. Check Flags value for ADS_FLAG_OBJECT_TYPE_PRESENT flag.
  11. If Flags is set to ADS_FLAG_OBJECT_TYPE_PRESENT, call the IADsAccessControlEntry::get_ObjectType method to get a string containing the rightsGUID of the extended right that the ACE applies to.
  12. Call the IADsAccessControlEntry::get_AceType method to get the type of ACE. The type will be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT to grant the trustee the extended right or ADS_ACETYPE_ACCESS_DENIED_OBJECT to deny the extended right.
  13. Call the IADsAccessControlEntry::get_Trustee method to get the security principal (user, group, computer, and so on) to whom the ACE applies.
  14. When you are done with for the ObjectType and Trustee strings, use SysFreeString to free the memory for those strings.
  15. When you are done with the interfaces, call Release to decrement or release all the interface references.

The following code fragment is a function that checks for a specified extended right in an ACE in the ACL of the specified object:

//DESCRIPTION: ReadExtendedRight checks for the specified extended right on the specified object.
//If an ACE with that extended right exists, it displays (using printf)
//the trustee and ACE type for the extended right.

//FLOW: Get the security descriptor of an object, get the ACL,
//enumerate the ACEs, check for extended rights ACEs,
//check for the specified right, and display the trustee and ACE type.

//NOTES: The pszRightsGUID UNICODE string should be a string
//containing the rightsGUID property value of the extended right
//and the string should have the same format as the COM Library
//function StringFromGUID2.
//For example:
//LPOLESTR pszRightsGUID = L"{8186e976-4d8a-11d2-95dd-0000f875b660}";
//The pbExists parameter specifies a BOOL that will receive
//TRUE if an ACE with the specified right exists; otherwise, FALSE.

HRESULT ReadExtendedRight(
                          IADs *pObject,
                          LPOLESTR pszRightsGUID,
                          BOOL *pbExists
                          )

{

BOOL bExists = FALSE;
VARIANT var,varACE;
HRESULT hr = E_FAIL;
IADsSecurityDescriptor *pSD = NULL;
IADsAccessControlList *pACL = NULL;
IDispatch *pDisp = NULL;
LPUNKNOWN pUnk = NULL;
ULONG lFetch;
BSTR szObjectType, szTrustee;
IEnumVARIANT *pEnum = NULL;
IADsAccessControlEntry *pACE = NULL;
IDispatch *pDispatch = NULL;
long lAceType, lAccessMask, lTypeFlag;

if (pObject)
{
    VariantClear(&var);
    //Get the nTSecurityDescriptor
    LPOLESTR szAttribute = L"nTSecurityDescriptor";
    hr = pObject->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
        if (var.vt==VT_DISPATCH)
        {
            //Use V_DISPATCH macro to get the IDispatch pointer from VARIANT structure
            //and QI for IADsSecurityDescriptor ptr.
            hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)&pSD);
            if (SUCCEEDED(hr))
            {
                //Get the DACL
                hr = pSD->get_DiscretionaryAcl(&pDisp);
                if (SUCCEEDED(hr))
                {
                    //QI for IADsAccessControlList interface
                    hr = pDisp->QueryInterface(IID_IADsAccessControlList,(void**)&pACL);
                    if (SUCCEEDED(hr))
                    {
                        //Enumerate the ACEs in the ACL.
                        hr = pACL->get__NewEnum( &pUnk );
                        if (SUCCEEDED(hr))
                        {
                            hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
                            if (SUCCEEDED(hr))
                            {
                                hr = pEnum->Next( 1, &varACE, &lFetch );
                                //Loop to read all ACEs on the object.
                                while( hr == S_OK )
                                {
                                    //Check if 1 item is returned and returned item is an IDispatch pointer.
                                    if ( (lFetch == 1) && (varACE.vt==VT_DISPATCH))
                                    {
                                        pDispatch = V_DISPATCH(&varACE);
                                        //QI for IADsAccessControlEntry to use to read the ACE.
                                        hr = pDispatch->QueryInterface( IID_IADsAccessControlEntry, (void**)&pACE );
                                        if (SUCCEEDED(hr))
                                        {
                                            hr = pACE->get_AccessMask(&lAccessMask);
                                            //Check for control access right flag to see if this is an ACE for an extended right.
                                            if (lAccessMask & ADS_RIGHT_DS_CONTROL_ACCESS)
                                            {
                                                pACE->get_Flags(&lTypeFlag);
                                                //Check to make sure this ACE applies to an object
                                                if (lTypeFlag & ADS_FLAG_OBJECT_TYPE_PRESENT)
                                                {
                                                    //Get the object type GUID and print it.
                                                    pACE->get_ObjectType(&szObjectType);
                                                    if ( _wcsicmp(szObjectType,pszRightsGUID) == 0 )
                                                    {
                                                        if (bExists==FALSE)
                                                            bExists = TRUE;
                                                        printf("\nObjectType: %S\n", szObjectType);
                                                        hr = pACE->get_AceType(&lAceType);
                                                        if (lAceType == ADS_ACETYPE_ACCESS_ALLOWED_OBJECT)
                                                            printf("ACE Type: ADS_ACETYPE_ACCESS_ALLOWED_OBJECT\n");

                                                        if (lAceType == ADS_ACETYPE_ACCESS_DENIED_OBJECT)
                                                            printf("ACE Type: ADS_ACETYPE_ACCESS_DENIED_OBJECT\n");

                                                        //Get the trustee (who the right applies to) and print it.
                                                        pACE->get_Trustee(&szTrustee);
                                                        printf("Trustee: %S\n", szTrustee);
                                                        //Free the string.
                                                        if (szTrustee)
                                                           SysFreeString(szTrustee);
                                                    }
                                                    //Free the string.
                                                    if (szObjectType)
                                                       SysFreeString(szObjectType);
                                                }
                                            }
                                        }
                                        //Clean up the enumerated ACE item.
                                        if (pACE)
                                            pACE->Release();
                                        if (pDispatch)
                                            pDispatch->Release();
                                    }
                                    //Clean up the VARIANT for the ACE item.
                                    VariantClear(&varACE);
                                    //Get the next ACE
                                    hr = pEnum->Next( 1, &varACE, &lFetch );
                                };//End of While loop
                            }
                            //Clean up
                            if (pEnum)
                               pEnum->Release();
                        }
                        if (pUnk)
                           pUnk->Release();
                    }
                    if (pACL)
                       pACL->Release();
                }
                if (pDisp)
                   pDisp->Release();
            }
            if (pSD)
               pSD->Release();
        }
    }
    VariantClear(&var);
}
*pbExists = bExists;
return hr;
}


Creating an Extended Right

To add an extended right to Active Directory, you create a controlAccessRight object in the Extended-Rights container.

When you create a controlAccessRight object, you must also set the following attributes to make the object a legal extended right that is recognized by Active Directory and the Windows NT® Security system:

  • cn. This attribute specifies the name of the controlAccessRight object in the directory. This will be the object's RDN within the Extended-Rights container.
  • appliesTo. This attribute specifies the list of object classes that the extended right applies to. In the list, an object class is represented by the schemaIDGUID property for its schemaClass object. The GUIDs are stored as strings of the form produced by the StringFromGUID2 function in the COM library — but without the starting and terminating curly braces ({ }). For example, the following GUID is the schemaIDGUID for the Computer class:
    bf967a86-0de6-11d0-a285-00aa003049e2

    Note that the schemaIDGUID property of a schemaClass has a syntax of octet string. The schemaIDGUID is stored as a binary GUID. If your application reads this property in order to get the schemaIDGUID to set for the appliesTo property of an extended right, you will need to convert the binary GUID to a string using the StringFromGUID2 function and then remove the curly braces.

displayName
This attribute specifies the string used to display the extended right in Security property page and other places in the Active Directory Users and Computers.
rightsGUID
This attribute specifies the GUID used to represent the extended right within an ACE. The GUID is stored as a string of the form produced by the StringFromGUID2 function in the COM library — but without the starting and terminating curly braces. Use UUIDGEN.EXE or some other utility to generate a GUID for the extended right.
objectClass
This attribute specifies controlAccessRight as the object class.

The following code fragment is a function that creates an extended right in the logged on user's domain:

HRESULT CreateExtendedRight(
                            LPOLESTR pszCommonName,
                            LPOLESTR pszDisplayName,
                            LPOLESTR pszRightsGUID,
                            ADSVALUE *pADSV,
                            int cAppliesTo
                            )

{
HRESULT hr = E_FAIL;

LPOLESTR pszRightRelPath = new WCHAR[MAX_PATH];
wcscpy(pszRightRelPath, L"cn=");
wcscat(pszRightRelPath, pszCommonName);

LPOLESTR szADsPath = new OLECHAR[MAX_PATH];
IADs *pObj = NULL;
IDispatch *pNewObject = NULL;
IDirectoryObject *pDO2 = NULL;
VARIANT var;

hr = ADsOpenObject(L"LDAP://rootDSE",
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)&pObj);

if (SUCCEEDED(hr))
{
    //Get the DN to the config container.
    hr = pObj->Get(L"configurationNamingContext",&var);
    if (SUCCEEDED(hr))
    {
        //Build ADsPath string to Extended-Rights container
        wcscpy(szADsPath,L"LDAP://cn=Extended-Rights,");
        wcscat(szADsPath,var.bstrVal);
        //Get an IDirectory Object pointer to the Extended Rights Container.
        hr = ADsOpenObject(szADsPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IDirectoryObject,
                         (void**)&pDO2);

        if (SUCCEEDED(hr))
        {
            // Count of attributes that must be set to create an extended right.
            const int cAttributes = 5;
            //Set attributes for new extended rights object.
            PADS_ATTR_INFO pAttributeEntries = new ADS_ATTR_INFO[cAttributes];
            //Set first attribute: CN

                pAttributeEntries[0].pszAttrName = L"CN"; //Attribute name: CN
                pAttributeEntries[0].dwControlCode = ADS_ATTR_APPEND; //Add the attribute.
                pAttributeEntries[0].dwADsType = ADSTYPE_CASE_IGNORE_STRING; //Attribute syntax is string.

                //Create the value struct--CN is a single value and of type string.
                ADSVALUE sCN;
                sCN.dwType = ADSTYPE_CASE_IGNORE_STRING;
                sCN.CaseIgnoreString = pszCommonName;

                //Add value to attribute entry.
                pAttributeEntries[0].pADsValues = &sCN;
                //Total number of values. Single value for CN.
                pAttributeEntries[0].dwNumValues = 1;

            //Set second attribute: objectClass
                pAttributeEntries[1].pszAttrName = L"objectClass"; //Attribute name: objectClass
                pAttributeEntries[1].dwControlCode = ADS_ATTR_APPEND; //Add the attribute.
                pAttributeEntries[1].dwADsType = ADSTYPE_CASE_IGNORE_STRING; //Attribute syntax is string.

                //Create the value struct-objectClass takes a single value of type string.
                ADSVALUE sObjectClass;
                sObjectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
                sObjectClass.CaseIgnoreString = L"controlAccessRight"; //objectClass is controlAccessRight

                //Add value to attribute entry.
                pAttributeEntries[1].pADsValues = &sObjectClass;
                //Total number of values. Single value in this case.
                pAttributeEntries[1].dwNumValues = 1;

            //Set third attribute: appliesTo
                pAttributeEntries[2].pszAttrName = L"appliesTo"; //Attribute name: appliesTo
                pAttributeEntries[2].dwControlCode = ADS_ATTR_APPEND; //Add the attribute.
                pAttributeEntries[2].dwADsType = ADSTYPE_CASE_IGNORE_STRING; //Attribute syntax is string.

                //Normally, create the value struct--appliesTo is multi-value and of type string.
                //Each value for this property is a schemaIDGUID of a class to which the right can be applied.
                //In this function, take the pointer to the array of ADSVALUE structures passed in.

                //Add value to attribute entry.
                pAttributeEntries[2].pADsValues = pADSV;
                //Total number of values. Two values for this one.
                pAttributeEntries[2].dwNumValues = cAppliesTo;
            //Set fourth attribute: displayName
                pAttributeEntries[3].pszAttrName = L"displayName"; //Attribute name: CN
                pAttributeEntries[3].dwControlCode = ADS_ATTR_APPEND; //Add the attribute.
                pAttributeEntries[3].dwADsType = ADSTYPE_CASE_IGNORE_STRING; //Attribute syntax is string.

                //Create the value struct--displayName is a single value and of type string.
                ADSVALUE sdisplayName;
                sdisplayName.dwType = ADSTYPE_CASE_IGNORE_STRING;
                sdisplayName.CaseIgnoreString = pszDisplayName;

                //Add value to attribute entry.
                pAttributeEntries[3].pADsValues = &sdisplayName;
                //Total number of values. Single value for this one.
                pAttributeEntries[3].dwNumValues = 1;

            //Set fifth attribute: rightsGUID
                pAttributeEntries[4].pszAttrName = L"rightsGUID"; //Attribute name: CN
                pAttributeEntries[4].dwControlCode = ADS_ATTR_APPEND; //Add the attribute.
                pAttributeEntries[4].dwADsType = ADSTYPE_CASE_IGNORE_STRING; //Attribute syntax is string.
                //Create the value struct--CN is a single value and of type string.
                ADSVALUE srg;
                srg.dwType = ADSTYPE_CASE_IGNORE_STRING;
                srg.CaseIgnoreString = pszRightsGUID;

                //Add value to attribute entry.
                pAttributeEntries[4].pADsValues = &srg;
                //Total number of values. Single value for this one.
                pAttributeEntries[4].dwNumValues = 1;

            //Create the controlAccessRight
            hr = pDO2->CreateDSObject(pszRightRelPath,     //Relative path of new object
                                     pAttributeEntries, //Attributes to be set
                                     cAttributes,  //Number of attributes being set
                                     &pNewObject //ptr to IDispatch on the new object to put a ptr to the object that was created.
                                     );
        }
    }
}

// Clean up
if (pObj)
  pObj->Release();
if (pDO2)
  pDO2->Release();
if (pNewObject)
  pNewObject->Release();

VariantClear(&var);
return hr;
}


Setting an Extended Right ACE in an Object's ACL

Using ADSI, you set an extended right ACE just as you would a property-specific ACE, except that the access mask has the ADS_RIGHT_DS_CONTROL_ACCESS flag and ObjectType is the rightsGUID of the extended right. Note that you can also use the Win32 security APIs to set ACLs on directory objects.

For extended rights, set the properties on the ACE in the following manner:

  • AccessMask must contain the following flag:

    ADS_RIGHT_DS_CONTROL_ACCESS

  • Flags is ADS_FLAG_OBJECT_TYPE_PRESENT
  • ObjectType is the string form of the rightsGUID property of the extended right. The string format of the GUID is the same string format as the StringFromGUID2 COM Library function
  • AceType is either ADS_ACETYPE_ACCESS_ALLOWED_OBJECT to grant the trustee the extended right or ADS_ACETYPE_ACCESS_DENIED_OBJECT to deny the trustee the extended right
  • Trustee is the security principal (user, group, computer, and so on) to whom the ACE applies

C/C++

If you are using ADSI, use the following steps for creating an ACE for an extended right and setting that ACE on the DACL of an object (see the following code fragment for an example):

  1. Get an IADs interface pointer to the object.
  2. Use the IADs::Get method to get the security descriptor of the object. The name of the property containing the security descriptor is nTSecurityDescriptor. The property will be returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsSecurityDescriptor interface to use the methods on that interface to access the security descriptor's ACL.
  3. Use the IADsSecurityDescriptor::get_DiscretionaryAcl method to get the ACL. The method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface to use the methods on that interface to access the individual ACEs in the ACL.
  4. Use CoCreateInstance to create the ADSI COM object for the new ACE and get an IADsAccessControlEntry interface pointer to that object. Note that the class ID is CLSID_AccessControlEntry.
  5. Set the properties of the ACE using the IADsAccessControlEntry methods:
    1. Use IADsAccessControlEntry::put_Trustee to set the trustee to whom this ACE applies. The trustee is a user, group, or other security principal. Your application should use the value from the appropriate property from the user or group object of the trustee to whom you want to apply the ACE. The trustee is specified as a BSTR and can take the following forms:

      Domain account (downlevel logon name) of the form domain\useraccount where domain is the name of the Windows NT® domain that contains the user and useraccount is the sAMAccountName property of the specified user or group. For example: arcadiabay\jsmith.

      Well-known security principal that represent special identities defined by the Windows NT® security system, such as everyone, local system, principal self, authenticated user, creator owner, and so on. The objects representing the Well-Known Security Principals are stored in the WellKnown Security Principals container beneath the Configuration container. For example: Anonymous Logon.

      Built-in group that represent the built-in user groups defined by the Windows NT® security system. They have the form BUILTIN\groupname where groupname is the name of the built-in user group. The objects representing the built-in groups are stored in the Builtin container beneath the domain container. For example: BUILTIN\Administrators.

      SID (string format) of the specified user, which is the objectSID property of the specified user. You can convert to string form using the ConvertSidToStringSid function in the Win32 Security API. For example: S-1-5-32-548.

      Distinguished Name of the specified user, which is the distinguishedName property of the specified user. For example: CN=J Smith,OU=Northwest,dc=arcadiabay,DC=Com.

    2. Use the IADsAccessControlEntry::put_AccessMask method to set the mask that specifies the access right. For ACEs that apply to extended rights, you must specify the following flag in the mask:

      ADS_RIGHT_DS_CONTROL_ACCESS

    3. Use the IADsAccessControlEntry::put_AceType method to specify whether to allow or deny access to the extended right. For extended rights in a DACL, this must be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
    4. Use the IADsAccessControlEntry::put_Flags method with the ADS_FLAG_OBJECT_TYPE_PRESENT flag. If this ACE should be inherited, set both the ADS_FLAG_OBJECT_TYPE_PRESENT and ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT flags.
    5. Use the IADsAccessControlEntry::put_ObjectType method to specify the GUID in the rightsGUID property of the controlAccessRights object that represents the extended right. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
    6. Use the IADsAccessControlEntry::put_AceFlags method to specify whether other objects beneath the specified object can inherit the ACE. The ADS_ACEFLAG_ENUM enumeration specifies the inheritance flags you can set on an ACE.
    7. If Flags contains ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT, use the IADsAccessControlEntry::put_InheritedObjectType method to specify the GUID of the schemaIDGUID property of the classSchema object that identifies the type of object. The GUID must be specified as a string of the form produced by the StringFromGUID2 function in the COM library.
  6. Use the QueryInterface method on the IADsAccessControlEntry object to get an IDispatch pointer. The AddAce method requires an IDispatch interface pointer to the ACE.
  7. Use the IADsAccessControlList::AddAce method to add the new ACE to the DACL.
  8. Use the IADsSecurityDescriptor::put_DiscretionaryAcl method to write the DACL containing new ACE to the security descriptor.
  9. Use the IADs::Put method to write the security descriptor to the object's nTSecurityDescriptor property to the property cache.
  10. Use the IADs::SetInfo method to update the property on the object in the directory.

The following code fragment is a function that adds an ACE for an extended right to the ACL of the specified object:

HRESULT SetExtendedRight(
                          IADs *pObject,
                          LPOLESTR pszRightsGUID,
                          LONG lAccessType,
                          LONG fInheritanceFlags,
                          LONG fAppliesToObjectType,
                          LPOLESTR szTrustee
                          )

{

BOOL bExists = FALSE;
VARIANT var,varACE;
HRESULT hr = E_FAIL;
IADsSecurityDescriptor *pSD = NULL;
IADsAccessControlList *pACL = NULL;
IDispatch *pDisp = NULL;
LPUNKNOWN pUnk = NULL;
ULONG lFetch;
IEnumVARIANT *pEnum = NULL;
IADsAccessControlEntry *pACE = NULL;
IDispatch *pDispatch = NULL;
IDispatch *pDispACE = NULL;

//Check params
//This sample takes the rightsGUID in StringFromGUID2 format and assumes it is the GUID for the correct extended right.
//For extended rights in a DACL, lAccessType must be ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
if ((lAccessType!=ADS_ACETYPE_ACCESS_ALLOWED_OBJECT)&&(lAccessType!=ADS_ACETYPE_ACCESS_DENIED_OBJECT))
    return E_INVALIDARG;
//This sample takes the szTrustee in an expected naming format and assumes it is the name for the correct trustee.
if (!szTrustee)
    return E_INVALIDARG;
if (!pszRightsGUID)
    return E_INVALIDARG;

if (pObject)
{

    VariantClear(&var);
    //Get the nTSecurityDescriptor
    LPOLESTR szAttribute = L"nTSecurityDescriptor";
    hr = pObject->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_DISPATCH--an IDispatch ptr to the security descriptor object.
        if (var.vt==VT_DISPATCH)
        {
            //Use V_DISPATCH macro to get the IDispatch pointer from the VARIANT structure
            //and QI for IADsSecurityDescriptor ptr.
            hr = V_DISPATCH( &var )->QueryInterface(IID_IADsSecurityDescriptor,(void**)&pSD);
            if (SUCCEEDED(hr))
            {
                //Get the DACL
                hr = pSD->get_DiscretionaryAcl(&pDisp);
                if (SUCCEEDED(hr))
                {
                    //QI for IADsAccessControlList interface
                    hr = pDisp->QueryInterface(IID_IADsAccessControlList,(void**)&pACL);
                    if (SUCCEEDED(hr))
                    {
                        //Create the COM object for the new ACE.
                        hr  = CoCreateInstance(
                                                    CLSID_AccessControlEntry,
                                                    NULL,
                                                    CLSCTX_INPROC_SERVER,
                                                    IID_IADsAccessControlEntry,
                                                    (void **)&pACE
                                                  );
                        if (SUCCEEDED(hr))
                        {
                            //Set the properties of the new ACE.
                            //For extended right, set the mask to ADS_RIGHT_DS_CONTROL_ACCESS.
                            hr = pACE->put_AccessMask(ADS_RIGHT_DS_CONTROL_ACCESS);
                            //Set the trustee.
                            hr = pACE->put_Trustee( szTrustee );
                            //For extended rights, set AceType to ADS_ACETYPE_ACCESS_ALLOWED_OBJECT or ADS_ACETYPE_ACCESS_DENIED_OBJECT.
                            hr = pACE->put_AceType( lAccessType );
                            //For this sample, set AceFlags so that ACE is not inherited by child objects.
                            hr = pACE->put_AceFlags(fInheritanceFlags);
                            //Flags specifies whether the ACE applies to the current object, child objects, or both.
                            //For this sample, fAppliesToInheritedObject is set to ADS_FLAG_OBJECT_TYPE_PRESENT so that the right applies only to the current object.
                            hr = pACE->put_Flags( fAppliesToObjectType );
                            //For extended rights, set ObjectType to the rightsGUID of the extended right.
                            if (fAppliesToObjectType & ADS_FLAG_OBJECT_TYPE_PRESENT)
                            {
                                hr = pACE->put_ObjectType( pszRightsGUID );
                            }
                            //Set the inherited object type if right applies to child objects.
                            if (fAppliesToObjectType & ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT)
                            {
                                hr = pACE->put_InheritedObjectType( pszRightsGUID );
                            }
                            //Add the ACE to the ACL.
                            //Need to QI for IDispatch pointer to pass to the AddAce method.
                            hr = pACE->QueryInterface(IID_IDispatch,(void**)&pDispACE);
                            if (SUCCEEDED(hr))
                            {
                                hr = pACL->AddAce(pDispACE);
                                if (SUCCEEDED(hr))
                                {
                                    //Write the DACL
                                    hr = pSD->put_DiscretionaryAcl(pDisp);
                                    if (SUCCEEDED(hr))
                                    {
                                        //Write the ntSecurityDescriptor property to the property cache.
                                        hr = pObject->Put(szAttribute, var);
                                        if (SUCCEEDED(hr))
                                        {
                                            //Call SetInfo to update the property on the object in the directory.
                                            hr = pObject->SetInfo();
                                        }
                                    }

                                }

                            }
                            if (pDispACE)
                                pDispACE->Release();
                        }
                        if (pACE)
                            pACE->Release();
                    }

                    if (pACL)
                       pACL->Release();
                }
                if (pDisp)
                   pDisp->Release();
            }
            if (pSD)
               pSD->Release();
        }
    }
    VariantClear(&var);
}
return hr;
}


Checking an Extended Right in an Object's ACL

To check an extended right on an object's ACL, use the AccessCheckByTypeResultList function. To use this function, your application needs a pointer to the SECURITY_DESCRIPTOR for the object (instead of an IADsSecurityDescriptor interface to an ADSI security descriptor COM object). If you are using ADSI, use the following steps for checking access for an extended right on an object (see the following code fragment for an example):

  1. Get an IDirectoryObject interface pointer to the object.
  2. Use the IDirectoryObject::Get method to get the security descriptor of the object. The name of the property containing the security descriptor is nTSecurityDescriptor. The property will be returned as a VARIANT containing an IDispatch pointer (the vt member is VT_DISPATCH). Call QueryInterface on that IDispatch pointer to get an IADsSecurityDescriptor interface to use the methods on that interface to access the security descriptor's ACL.
  3. Use the IADsSecurityDescriptor::get_DiscretionaryAcl method to get the ACL. The method returns an IDispatch pointer. Call QueryInterface on that IDispatch pointer to get an IADsAccessControlList interface to use the methods on that interface to access the individual ACEs in the ACL.
  4. Get the token of the client whose access you want to check.
  5. Use the AccessCheckByTypeResultList function to check the permissions for the specified extended right for the specified client.

The following code fragment is a function that checks whether the currently logged-on user has permissions for an extended right on the specified object:

// Define the Generic Mapping structure.
// generic read
#define GENERIC_READ_MAPPING     ((STANDARD_RIGHTS_READ)     | \
                                  (ADS_RIGHT_ACTRL_DS_LIST)   | \
                                  (ADS_RIGHT_DS_READ_PROP)   | \
                                  (ADS_RIGHT_DS_LIST_OBJECT))
// generic execute
#define GENERIC_EXECUTE_MAPPING  ((STANDARD_RIGHTS_EXECUTE)  | \
                                  (ADS_RIGHT_ACTRL_DS_LIST))
// generic right
#define GENERIC_WRITE_MAPPING    ((STANDARD_RIGHTS_WRITE)    | \
                                  (ADS_RIGHT_DS_SELF)      | \
                  (ADS_RIGHT_DS_WRITE_PROP))
// generic all
#define GENERIC_ALL_MAPPING      ((STANDARD_RIGHTS_REQUIRED) | \
                                  (ADS_RIGHT_DS_CREATE_CHILD)    | \
                                  (ADS_RIGHT_DS_DELETE_CHILD)    | \
                                  (ADS_RIGHT_DS_DELETE_TREE)     | \
                                  (ADS_RIGHT_DS_READ_PROP)   | \
                                  (ADS_RIGHT_DS_WRITE_PROP)  | \
                                  (ADS_RIGHT_ACTRL_DS_LIST)   | \
                                  (ADS_RIGHT_DS_LIST_OBJECT)     | \
                                  (ADS_RIGHT_DS_CONTROL_ACCESS)  | \
                                  (ADS_RIGHT_DS_SELF))
// Standard DS generic access rights mapping
#define DS_GENERIC_MAPPING {GENERIC_READ_MAPPING,    \
                GENERIC_WRITE_MAPPING,   \
                GENERIC_EXECUTE_MAPPING, \
                GENERIC_ALL_MAPPING}

HRESULT CheckExtendedRight(
                            HANDLE hToken,
                            IDirectoryObject *pDO2,
                            LPOLESTR pszRightsGUID,
                            DWORD *dwAccess
                            )

{
HRESULT hr = E_FAIL;
*dwAccess = FALSE;
BOOL bSuccess = FALSE;
PADS_ATTR_INFO pAttrInfo = NULL;
DWORD   dwReturn= 0;
LPWSTR   pAttrNames[]= {L"nTSecurityDescriptor",L"objectSid"};
PSECURITY_DESCRIPTOR pSD = NULL;
DWORD   SDSize;
VOID    *pAbsoluteSD = NULL;
DWORD   AbsoluteSDSize = 0;
VOID    *pDacl = NULL;
DWORD   DaclSize = 0;
VOID    *pSacl = NULL;
DWORD   SaclSize = 0;
VOID    *pOwner = NULL;
DWORD   OwnerSize = 0;
VOID    *pGroup = NULL;
DWORD   GroupSize = 0;
PSID pSID = NULL;
// Get attributes for security descriptor and SID
hr = pDO2->GetObjectAttributes( pAttrNames,
                                  2,
                                  &pAttrInfo,
                                  &dwReturn );
if ( (SUCCEEDED(hr)) && (dwReturn>0) )
{
   for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
   {
       //Check the attribute name
       if ( _wcsicmp(pAttrInfo->pszAttrName,L"nTSecurityDescriptor") == 0 )
       {
           //Check the attribute type.
           if (pAttrInfo->dwADsType==ADSTYPE_NT_SECURITY_DESCRIPTOR)
           {
               pSD = (PSECURITY_DESCRIPTOR)(pAttrInfo->pADsValues->SecurityDescriptor.lpValue);
               SDSize = (pAttrInfo->pADsValues->SecurityDescriptor.dwLength);
           }
       }
       if ( _wcsicmp(pAttrInfo->pszAttrName,L"objectSID") == 0 )
       {
           //Check the attribute type.
           if (pAttrInfo->dwADsType==ADSTYPE_OCTET_STRING)
           {
               pSID = (PSID)(pAttrInfo->pADsValues->OctetString.lpValue);
           }
       }
    }
    OBJECT_TYPE_LIST sObjectList;
    sObjectList.Level = ACCESS_OBJECT_GUID;
    sObjectList.Sbz = 0;
    CLSID pclsid;
    //Make the rightsGUID string the right format
    //for conversion with the COM conversion functions.
    LPOLESTR pszGUID = new OLECHAR[MAX_PATH];
    wcscpy(pszGUID, L"{");
    wcscat(pszGUID, pszRightsGUID);
    wcscat(pszGUID, L"}");
    hr = CLSIDFromString(
              pszGUID,  //Pointer to the string representation of the CLSID
              &pclsid  //Pointer to the CLSID
            );
    if (SUCCEEDED(hr))
    {
       sObjectList.ObjectType = (GUID*)&pclsid;
    }
    else
        return E_FAIL;

    CHAR PrivilegeSetBuffer[256];
    PRIVILEGE_SET *PrivilegeSet = (PRIVILEGE_SET *)PrivilegeSetBuffer;
    DWORD dwPrivSetSize = sizeof( PrivilegeSetBuffer );
    DWORD GrantedAccess = 0;
    ZeroMemory(PrivilegeSetBuffer, 256);
    DWORD DesiredAccess = ADS_RIGHT_DS_CONTROL_ACCESS;
    // Use the GENERIC_MAPPING structure to convert any
    // generic access rights to object-specific access rights.
    GENERIC_MAPPING GenericMapping = DS_GENERIC_MAPPING;
    // Before calling AccessCheck, a convert must be done
    // security descriptor into Absolute form.
    if( ! MakeAbsoluteSD(
                      pSD,
                      (PSECURITY_DESCRIPTOR)pAbsoluteSD,
                      &AbsoluteSDSize,
                      (PACL)pDacl,
                      &DaclSize,
                      (PACL)pSacl,
                      &SaclSize,
                      (PSID)pOwner,
                      &OwnerSize,
                      (PSID)pGroup,
                      &GroupSize
                      ))
    {
        pAbsoluteSD = (PSECURITY_DESCRIPTOR)LocalAlloc(0,AbsoluteSDSize);
        if(!pAbsoluteSD)
        {
            // TODO: handle this.
        }
        pDacl = (PACL)LocalAlloc(0,DaclSize);
        if(!pDacl)
        {
            // TODO: handle this.
        }
        pSacl = (PACL)LocalAlloc(0,SaclSize);
        if(!pSacl)
        {
            // TODO: handle this.
        }
        pOwner = (PSID)LocalAlloc(0,OwnerSize);
        if(!pOwner)
        {
            // TODO: handle this.
        }
        pGroup = (PSID)LocalAlloc(0,GroupSize);
        if(!pGroup)
        {
            // TODO: handle this.
        }
        if( ! MakeAbsoluteSD(
                          pSD,
                          (PSECURITY_DESCRIPTOR)pAbsoluteSD,
                          &AbsoluteSDSize,
                          (PACL)pDacl,
                          &DaclSize,
                          (PACL)pSacl,
                          &SaclSize,
                          (PSID)pOwner,
                          &OwnerSize,
                          (PSID)pGroup,
                          &GroupSize
                  ))
        {
            //
            // TODO: handle this
            //
            //Clean up and return
            if (pAttrInfo)
              FreeADsMem( pAttrInfo );
            return E_FAIL;

        }
    }

    bSuccess = AccessCheckByTypeResultList(
                      pSD,   // security descriptor
                      pSID,            // SID of object being checked
                      hToken,               // handle to client access token
                      DesiredAccess,              // requested access rights
                      &sObjectList, // array of object types
                      1,       // number of object type elements
                      &GenericMapping,  // map generic to specific rights
                      PrivilegeSet,      // receives privileges used
                      &dwPrivSetSize,       // size of privilege-set buffer
                      &GrantedAccess,     // retrieves mask of granted rights
                      dwAccess        // retrieves results of access check
                      );
    //Check if access check function call succeeded.
    if(bSuccess)
    {
        hr = S_OK;
    }
    else
        hr = E_FAIL;
}
// Use FreeADsMem for all memory obtained from ADSI call
if (pAttrInfo)
      FreeADsMem( pAttrInfo );

return hr;
}


Extending the Schema

Active Directory™ has an extensible schema. The schema defines the attributes and classes that can be used in Active Directory.

This section discusses the following topics:

  • Definitions for the schema, classes, attributes, syntaxes, and OIDs.
  • How to bind to the schema container and read the properties of existing classes and attributes.
  • How and when to extend the schema by defining new attributes and classes.
  • How to install schema extensions using LDIFDE, CSVDE, or programmatically with ADSI.


Schema Overview

The schema is a formal definition of every object class that can be created in the directory, the attributes that describe each object class, and the possible parents for every object class. An object in the directory is a representation of some entity in the "real" world, such as a user, a printer, a network service, an application program, and so on. The objects in the directory have attributes that describe them. For example, a user has attributes like "given name", "surname", and "street address" among others. The "kind" of thing an object represents is called the object's class. The definition for each class of object contains a list of the attributes that can be used to describe that object. The list of attributes is divided into those that an object of that class must contain, and additional attributes that the object may contain. The directory is arranged in a hierarchy; the definition of each class also lists the classes whose objects can be parents of objects of a given class — this controls the shape of the directory hierarchy.

The schema also formally defines each attribute. The definition for each attribute contains a unique name for the attribute, the syntax for the attribute (that is, the data type for the data the attribute contains), any range limits for the attributes values (if applicable), whether the attribute can have only one value or multiple values, and whether the attribute is indexed. The directory schema defines each attribute exactly once —object class definitions contain references to the attributes defined in the schema. For example, the "description" attribute is defined only once and is referenced by many object classes. This is different than a relational database schema, where the "attributes" (columns) are separately defined for each table.


Schema Implementation

In Active Directory™, class and attribute definitions are stored in the directory as instances of the classSchema and attributeSchema classes, respectively. ClassSchema and attributeSchema are themselves classes defined in the schema. This means that to manipulate the Active Directory schema, you can use the same LDAP operations that you use to manipulate any other object. Since the schema is a key part of the directory that affects the entire forest, the schema has special restrictions, which are covered in this chapter.

To summarize the schema implementation:

  • Instances of the classSchema class are used to define every object class supported by Active Directory. The attributes of a classSchema object (for instance, its May-Contain and Must-Contain attributes) describe an object class, the same way the attributes of a user object (for instance, its Given-Name and Surname attributes) describe that user.
  • Instances of the class attributeSchema are used to define every attribute supported by Active Directory. The attributes of an attributeSchema object (for instance, its attributeSyntax and isSingleValued attributes) describe an attribute, the same way the attributes of a user object describe that user.
  • AttributeSchema and classSchema instances are stored in a well-known place in the directory, the schema container. The schema container always has a distinguished name of the form:
    cn=Schema,cn=Configuration,<<Forest Root>>

    where <<Forest Root>> is the distinguished name of the root of the forest, for example, DC=Microsoft, DC=Com.

    To get a binding string for the schema container, read the schemaNamingContext property of rootDSE. For more information on rootDSE and its properties, see Serverless Binding and RootDSE.

When thinking about the schema, it is very important to remember two key points:

  • Schema changes are global. There is a single schema for an entire forest. The schema is globally replicated: a copy of the schema exists on every domain controller in the forest. When you extend the schema, you do so for the entire forest.
  • Schema additions are not reversible. Once a new class or attribute has been added to the schema, it cannot be removed. An existing attribute or class can be disabled, but not removed (see Disabling Existing Classes and Attributes). Disabling a class or attribute does not affect existing instances of the class or attribute, but it prevents new instances from being created. You cannot disable an attribute if it is included in any class that is not disabled.


Inheritance in the Active Directory Schema

Inheritance is the ability to build new object classes from existing object classes. The new object is defined as a subclass of the original object. The original object becomes a superclass of the new object. A subclass inherits the attributes of the superclass, including structure rules and content rules.

Structure rules define the possible tree structure. In Active Directory, the structure rules are completely expressed by the possSuperiors attribute present on each classSchema object.

Content rules define the possible attributes of the class instances stored in the directory service. In Active Directory, the content rules are completely expressed by the must-have and may-have attributes of the schema definitions for each class.

Inheritance is recursive. For example, contact is a subclass of organizationalPerson; organizationalPerson is a subclass of person; and person is a subclass of Top. So, the contact class inherits all the attributes from the organizationalPerson, person, and Top classes.

All object classes in the schema are derived from the special class Top. With the exception of Top, all object classes are subclasses of some other object class.


Root DSE

RFC 2251 defines the LDAP protocol and key characteristics of directories that are exposed using that protocol. An important characteristic of these directories is the Root DSE. "DSE" is an acronym for "DSA-specific Entry". These are entries that contain information specific to a particular directory server. The "root DSE" is just such an entry, and is the topmost such entry held by a given server, hence the term "root DSE". The root DSE is retrieved by performing a base DN search" of the null DN". The null DN is simply an empty distinguished name, a null string, "". A base DN Search tells the LDAP server to retrieve exactly one entry, whose DN is the DN provided. The null DN tells the LDAP server the entry you want is the root DSE.

RFC 2251 requires that the root DSE contain at minimum a set of attributes that tell the client important information about the directory held by the server. This includes the list of naming contexts held by the server, the DN of the schema container, the list of LDAP controls the server understands, and other essential information.

Since all servers must provide a root DSE and the root DSE must be readable by presenting the null DN a client can discover the root of the directory, the location of the schema, the version of LDAP supported, and other important information for itself when talking to an otherwise unknown server.


Schema Container

Attribute definitions are stored in objects of class attributeSchema. Object class definitions are stored in objects of class classSchema. The attributeSchema and classSchema objects are stored in a specific location in the directory, the Schema container. The schema container exists in the directory namespace as a child of the root domain of the forest in which it appears. For example, in the Microsoft.Com forest, the schema is located at the following site:

cn=Schema,cn=Configuration,dc=Microsoft,dc=Com

The schema container is a separate naming context and is replicated to every Domain Controller (DC) in the forest. There is a single schema container for the entire forest. The distinguished name of the Active Directory schema container can always be obtained by reading the schemaNamingContext attribute from the Root DSE.


Characteristics of Attributes

Attributes may be single-valued or multi-valued. Multi-valued attributes are unordered; there is no guarantee they will be stored or returned in any specific order. In the event of a replication collision (see Consistency GUIDs), conflict resolution is per attribute, not per value within an attribute. So the entire multi-value wins or loses.

Attributes can have length or range constraints. The constraints are specified by the rangeLower and rangeUpper attributes on the attributeSchema object. For attributes with numeric syntax the range specifies the minimum and maximum value. For attributes with string syntax the range specifies the minimum and maximum length. For attributes with binary syntax, the range specifies the number of bytes. If one constraint is present without the other, the missing constraint is unbounded. For example, if the rangeLower for an integer is 3, and rangeUpper is absent, it means there is no upper constraint on the attribute. Likewise, if rangeUpper for a string is 2000, and rangeLower is absent, it means there is now lower constraint on the length.


Indexed Attributes

Attributes may be indexed. Indexing an attribute can improve the performance of queries for that attribute.

  1. Attributes are indexed when the searchFlags property in their schema definition has the least significant bit set to "1". Changing the least significant bit of searchFlags to 1 will dynamically build an index; changing it to 0 will drop an index for the attribute in question. The index will be built automatically by a background thread on the DC.
  2. Ideally indexed properties should be single valued with highly unique values evenly distributed across the set of instances. The less unique the values of an attribute are, the less effective the index will be.
  3. Multi-valued properties can also be indexed although the cost to build the index is larger in terms of storage, update, and search time. The uniqueness requirement for a multi-valued property is the same as that for a single-valued property: the more unique the values are the more effective the index.
  4. The more indexed attributes a class has, the longer it takes to create new instances of the class.

Indices apply to attributes, not to classes. That is, when you mark an attribute as indexed, all instances of the attribute are added to the index, not just the instances that are members of a particular class.


Attributes Included in the Global Catalog

Attributes may be flagged for inclusion in the global catalog (GC). An attribute is replicated to in the GC if the IsMemberOfPartialAttributeSet attribute in its attributeSchema object is set to TRUE.

Attributes with the following characteristics are appropriate for storage in the global catalog:

  1. The attribute is "globally interesting". Either the attribute is needed for locating objects that may occur anywhere in the forest, or read access to the attribute is valuable even when the full object is not accessible. An example of the first type is the Location attribute, which can be used to find a Print-Queue object no matter where that object is located. An example of the second type is Telephone-Number, since you can phone someone even if you can't access a full replica of their User object.
  2. The volatility of the attribute is very low, since changes to the value will be replicated to all global catalog servers in the enterprise.
  3. The size of the attribute value is small. "Small" is highly subjective: when placing an attribute in the GC you must consider the impact of replicating the attribute to all GC servers in the enterprise. The smaller the attribute, the lower the impact. Since replication only occurs when the attribute changes, the impact of replication is also smaller as volatility decreases, so a large attribute with very low volatility may have a smaller overall impact than a small attribute with high volatility.

When deciding whether or not to place an attribute in the global catalog keep in mind that you are trading increased replication and increased disk storage on global catalog servers for (potentially) faster query performance. Attributes stored in the GC can be retrieved directly by searching the GC; contrast this with a two-step process for attributes not in the GC: first the user must search the GC to locate the object of interest, then read the attributes from a full replica of that object which are potentially located on some different server. Attributes that are frequently queried and referenced by users across the enterprise, such as employee names and phone numbers, are good candidates for inclusion in the global catalog. An infrequently referenced attribute such as "driverVersion" for printers are best left out of the GC.


Characteristics of Object Classes

The object class instance is the unit of storage in the directory. The following is true of all object instances in the directory:

  1. Each object is an instance of an object class defined in the schema.
  2. Each object instance (except the root of the tree, Root) has a parent.
  3. An object instance can have exactly one parent.
  4. An object is a container if it's class appears in the possSuperiors attribute of another class. Active Directory provides an additional attribute on object instances, possibleInferiors. This is a computed attribute (that is, it is not actually stored in the directory) that lists the classes that reference the current object's class as a possible superior using the possSuperiors attribute. If possibleInferiors is not null, the current object is a container, not a leaf. Except for schema objects (that is, objects of class classSchema or attributeSchema), any object in Active Directory may be a container. Specifically, any object that appears in the possSuperiors attribute of an object class definition is a container.
  5. All classes derived from other classes, except for "Top".


Object Class

The class of each object class instance is identified by the objectClass attribute. Once created, an object's class can never be changed. ObjectClass is a multi-valued attribute; it lists the specific class of the object, and the classes of all structural or abstract classes from which the specific class was derived, including Top, the class from which all other classes are ultimately derived. Active Directory does not list auxiliary classes in an object's objectClass attribute.

Note The objectClass attribute is not indexed. This is because it has multiple values and is highly non-unique; that is, every instance of the objectClass attribute includes "Top". This means an index would be very large and ineffective. To locate objects of a given class, use the objectCategory attribute, which is single-valued and indexed.


Object Category

The definition of a class in the schema includes a "default object category" (defaultObjectCategory). This is the name of a class in the schema that will be stored with instances of the current class. The default value for new class definitions is always the current class, that is, the default category for "organizationalUnit" is "organizationalUnit". Some classes reference another class as their defaultObjectCategory. This allows a query to readily find groups of related objects, even if they are of differing classes. For example, "User", "person", organizationalPerson", and "contact" all have "person" as the defaultObjectCategory. This allows a query for objects with objectCategory "person" to be located with a single query. Queries for "people" are very common, so this is a simple optimization.

Important When building queries for objects based on their class, use objectCategory, because it is indexed, not objectClass, which is not.


Object Names and Identities

In Active Directory, an object has several identities:

  • The distinguished name (DN) is the current name of the object. This is the distinguishedName property of the object. The Distinguished Name is a string, formed by concatenating the Relative Distinguished Names of the object and each of its ancestors all the way to Root. Distinguished Names are unique within a forest. The Distinguished Name changes when an object instance is moved or renamed.
  • The relative distinguished name (RDN) is the name defined by an object's naming attribute. The naming attribute is identified in the classSchema object for the class by the rDnAttID attribute. Most object classes use Common Name ("cn") as the naming attribute. An object's RDN must be unique in the container where the object resides. There can be many object instances with the same RDN, but no two can be in same container.
  • The Object GUID (objectGUID) is a globally unique identifier (GUID) assigned by Active Directory when the object instance is created. A GUID is a 128-bit number guaranteed to be unique in space and time. Object GUIDs never change – if an object is moved or renamed, the objectGUID remains the same. Applications that save references to Active Directory objects must use the objectGUID to be rename-safe. The Distinguished Name for an object might change, but the objectGUID will not.

Object instances can have many other attributes, and the attributes can be used for identification by applications. For example, security principals (users, computers, and groups) have User Principal Names, SAM Account Names, and Windows 2000 Security Identifiers. These attributes are very important "names" for Windows 2000 security, but these are not part of the object's identity from the directory's perspective.


Object Class Category

Do not confuse this with "object category", which is discussed above. Three categories of classes exist in Active Directory:

Structural
Structural classes are the only classes that can have instances in Active Directory. A structural class can be derived from either an abstract class or another structural class. A structural class can include any number of auxiliary classes in its definition.
Abstract
Abstract classes are templates used to derive new abstract, auxiliary, and structural classes. An auxiliary class can only be a subclass of an abstract class. Abstract classes cannot be instantiated in Active Directory.
Auxiliary
Auxiliary classes are like "include" files; an auxiliary class contains a list of attributes; adding the auxiliary class to the definition of a structural or abstract class adds the auxiliary class's mustContain and mayContain (as well as systemMustContain and systemMayContain) attributes to the definition. An auxiliary class cannot be instantiated in the directory. New auxiliary classes can be derived from existing auxiliary or structural classes.


Object Identifiers (OIDs)

Object Identifiers (OIDs) are unique numeric values, issued by various "Issuing Authorities" to uniquely identify data elements, syntaxes, and various other parts of distributed applications. OIDs are found in OSI applications, X.500 Directories, SNMP, and other applications where uniqueness is important. OIDs are based on a tree structure, in which a superior issuing authority (such as the ISO) allocates a "branch" of the tree to a subauthority, who in turn can allocate sub-branches.

The LDAP protocol (RFC 2251) requires a directory service to identify object classes, attributes, and syntaxes with OIDs. This is part of the LDAP X.500 legacy.

OIDs in Active Directory include some issued by the ISO for X.500 classes and attributes, and some issued by Microsoft and other issuing authorities. OID notation is a dotted string of numbers, for example 1.2.840.113556.1.5.4, which breaks down as in the following table.

Value | Description
1 | ISO - the "root authority", issued "1.2" to ANSI
2 | ANSI – issued "1.2.840" to USA
840 | USA – issued "1.2.840.113556" to Microsoft
113556 | Microsoft
1 | Microsoft - Active Directory Service
5 | Microsoft - Active Directory Service: Classes
4 | Microsoft - Active Directory Service: Classes: Builtin-Domain

Several methods for obtaining valid OIDs for use in extending the schema are described in Obtaining a Root Object Identifier (OID).


Reading the Schema

There are three ways to view the schema:

  • Bind to the schema container on a DC and read the classSchema and attributeSchema objects from that container. The schema container is replicated to every DC in the forest. This means the schema container is available on every DC. Note that the DN of the schema container should always be obtained by reading the schemaNamingContext attribute from the rootDSE.
  • Bind to abstract schema by binding to the subSchema object in the schema container and reading its properties. The properties contain a subset of the information stored in the classSchema and attributeSchema objects in the Schema container. The subSchema object has the name Aggregate and has the properties found in the following table.
Property | Meaning
attributeTypes | A multi-valued property containing strings that represent each attribute in the schema. Each value contains the attributeID, lDAPDisplayName, syntax, upper and lower range, and an item that indicates whether the attribute is multi-valued.
extendedAttributeInfo | A multi-valued property containing strings that represent additional information for each attribute. Each value contains the attributeID, lDAPDisplayName, schemaIDGUID, and attributeSecurityGUID.
objectClasses | A multi-valued property containing strings that represent each class in the schema. Each value contains the governsID, lDAPDisplayName, mustContain, mayContain, and so on.
extendedClassInfo | A multi-valued property containing strings that represent additional information for each class. Each value contains the governsID, lDAPDisplayName, and schemaIDGUID of the class.

To get the DN for the subSchema object, read the subSchemaSubEntry property of the rootDSE.

  • Bind to abstract schema using ADSI using the following abstract schema binding string format:

    LDAP://schema/object

    where object is the lDAPDisplayName of the class or attribute.

    The binding string LDAP://schema binds to the subSchema object and ADSI exposes the IADsContainer interface so that you can enumerate or retrieve the attributes and classes as ADSI COM objects. Or you can bind directly to the attribute or class in the abstract schema using the LDAP://schema/object format. For example, you would bind to the user class in the abstract schema using the following binding string:

    LDAP://schema/user

    ADSI provides the IADsProperty and IADsClass interfaces for easy access to the properties of attributes and classes in the abstract schema. When binding directly to attributes or classes, request an IADsProperty or IADsClass pointer. When binding to the subSchema object, request an IADsContainer pointer so that you can use the enumeration and GetObject methods to access the attributes and classes.

    In addition, the IADs interface provides the IADs::get_Schema method. This method returns the ADsPath for the object's class in abstract schema binding string format. So, if you have an IADs pointer to an object, you can bind to its class in the abstract schema using the ADsPath returned from IADs::get_Schema.


Guidelines for Binding to the Schema

Here are some guidelines for when to use each type of schema binding technique:

  • For adding or modifying attributes or classes, bind directly to the objects in the Schema container. Note that modifications must be made at the schema master.
  • For getting specific properties set on the attributeSchema and classSchema objects (especially those not exposed in the abstract schema), bind directly to the objects in the Schema container. For example, oMSyntax, attributeSyntax, defaultSecurityDescriptor, and other properties are not exposed in the abstract schema.
  • For querying for attributeSchema and classSchema objects, bind to the Schema container and perform a one level search. The abstract schema does not support query.
  • If you use the abstract schema, use ADSI and the IADsClass and IADsProperty interfaces. ADSI exposes classes and attributes as ADSI COM objects — this is much easier to read than the string values (which must be parsed to extract specific information about an attribute or class) contained in the properties from the subSchema object.
  • For getting common information about an attribute or class, use the abstract schema. For classes in particular, the abstract schema provides some information that would require multiple bindings and calculation if you tried to get that information from the attributeSchema and classSchema objects. For example, to get the optional and mandatory properties, you would have to traverse and collect the mayContain and mustContain values from all parent classes and auxiliary and abstract classes that a class is derived from. You can get this information by using the IADsClass::get_Mandatory method for the ADSI COM object representing a class in the abstract schema.

    In most cases, if the information is available in the abstract schema, you should get it from there.

    For classes, here are the key properties provided by the abstract schema:

Method (Property) | Meaning
IADsClass::get_Abstract | Indicates whether this is an abstract class.
IADsClass::get_Auxiliary | Indicates whether this is an auxiliary class.
IADsClass::get_AuxDerivedFrom | Array of auxiliary classes this class derives from.
IADsClass::get_Container | Indicates whether this class is a container object.
IADsClass::get_DerivedFrom | Array of classes this class is derived from
IADsClass::get_MandatoryProperties | Array of mandatory properties.
IADsClass::get_OID | governsID of the class.
IADsClass::get_OptionalProperties | Array of optional properties.
IADsClass::get_PossibleSuperiors | Array of possibleSuperiors


Reading attributeSchema and classSchema Objects

To read the properties of an attributeSchema or classSchema object, bind to that object and use the methods provided by the IADs or IADsPropertyList interfaces. In C/C++, you can also use IDirectoryObject to retrieve specific properties. For information on reading properties of an object, see Reading an Object's Properties.

If you are reading multiple attributeSchema or classSchema objects, you can increase performance by doing the following:

  1. Bind to the Schema container and get an IADsContainer interface pointer.
  2. Use the IADsContainer::GetObject method to bind to attributeSchema and classSchema objects. This saves the extra processing of credentials at the server for each GetObject operation. If you called ADsOpenObject or ADsGetObject repeatedly using explicit credentials, you would have to cache the credentials (username) to user for each call. If you use IADsContainer::GetObject, you avoid having to cache any credentials and retain the same security context established with the original bind on the Schema container. Note that attributeSchema and classSchema objects are one level deep in the Schema container; therefore, building the relative path for the IADsContainer::GetObject call is simple (for example, "cn=user" for the classSchema object for the user class).


attributeSchema Object Properties

The properties that an object of a certain type can contain are listed in its classSchema object. A property's type is defined by an attributeSchema object. An attributeSchema object has the following key properties:

cn (Common-Name)
The name of the attribute.
lDAPDisplayName (LDAP-Display-Name)
The name used by LDAP clients to read and write the attribute using the LDAP protocol. For information on composing a common-name and an LDAP display name for a new attribute, see Naming Attributes and Classes.

The LDAP-Display-Name property is unique across all LDAP-Display-Names for all Class-Schema and Attribute-schema objects in the Schema container.

schemaIDGUID (Schema-ID-GUID)
A GUID stored as an octet string. This GUID uniquely identifies the attribute. This GUID can be used in access control entries to control access to instances of this attribute.

On creation of the attributeSchema object, Active Directory™ generates this value if it is not specified. If you are creating a new attribute, it is recommended that you generate your own GUID for each attribute so that all installations of your extension will use the same schemaIDGUID to refer to the attribute.

attributeSecurityGUID (Attribute-Security-GUID)
A GUID stored as an octet string. This GUID identifies the attribute as a member of an attribute (or property) grouping. This GUID can be used in access control entries to control access to all attributes that have the specified GUID set in their attributeSecurityGUID property. This attribute is optional.
isSingleValued
A Boolean value that is TRUE if the attribute can have only one value or FALSE if the attribute can have multiple values. If this property is not set, the attribute has a single value.
adminDisplayName
The display name of the attribute if a mapping does not exist in the attributeDisplayNames property of the display specifier for the class that contains this attribute. By default, this value is the same as the cn value.
attributeID
The OID of this attribute. This value must be unique among the attributeIDs of all attributeSchema objects and governsIDs of all classSchema objects. See the section on OIDs.
attributeSyntax
The OID of the syntax for this attribute. The combination of the attributeSyntax and oMSyntax properties determines the syntax of the attribute. See the syntax table.
oMSyntax
An integer that is the XDS representation of the syntax. The combination of the attributeSyntax and oMSyntax properties determines the syntax of the attribute. See the syntax table.
oMObjectClass
An octet string that must be specified for attributes of oMSyntax 127. For attributes with any other oMSyntax value, this property is not used. If no oMObjectClass is specified for an attribute with an oMSyntax of 127, the default oMObjectClass is set. Usually, there is a one-to-one mapping between the attributeSyntax and the oMObject class. The following table lists the oMObjectClass values:
attributeSyntax | oMObjectClass
2.5.5.1 | \x2B0C0287731C00854A
2.5.5.7 \x56060102050B1D for OR-Name.

\x2A864886F7140101010B for DN-Binary.

Default is OR-Name if no oMObjectClass is specified.
2.5.5.13 | \x2B0C0287731C00855C
2.5.5.14 \x2B0C0287731C00853E for Access-Point.

\x2A864886F7140101010C for DN-String.

Default is Access-Point if no oMObjectClass is specified.

rangeLower
An integer that specifies the lower value of the range of values for this attribute. All values set for this attribute must be greater than or equal to this value. If both rangeLower and rangeUpper are set, rangeLower must be less than rangeUpper. For integers, value means the value of the integer. For string syntaxes, value means the number of characters in the string. For octet strings, value means the number of bytes.
rangeUpper
An integer that specifies the upper value of the range of values for this attribute. All values set for this attribute must be less than or equal to this value. If both rangeLower and rangeUpper are set, rangeLower must be less than rangeUpper. For integers, value means the value of the integer. For string syntaxes, value means the number of characters in the string. For octet strings, value means the number of bytes.
searchFlags
An integer value that contains bit flags that define whether the attribute is indexed if the least significant bit is set to 1, or non-indexed if the bit is zero.
isMemberOfPartialAttributeSet
A boolean value that defines whether the attribute is replicated to the global catalog (it is in the global catalog if it has a value of TRUE, and not in the global catalog if the value is FALSE).
linkID
An integer that indicates that the attribute is a linked attribute. An even integer is a forward link and an odd integer is a back link.

This value must be unique for linkIDs of all attributeSchema objects. A back link must have corresponding forward link.

A forward-link/back-link pair is identified by attributes with linkID x and x+1 where x > 1 and is an even integer.

Forward links must have attributeSyntax value 2.5.5.1, 2.5.5.7, or 2.5.5.14.

Back links can have attributeSyntax 2.5.5.1 only.

systemFlags
An integer value that contains flags define additional properties of the attribute such as whether attribute is constructed, non-replicated, and category 1 or 2. Use the ADS_SCHEMA_SYSTEMFLAG enumeration to decipher the flags.
systemOnly
A boolean value that specifies whether only Active Directory can modify the attribute.
mAPIID
An integer by which MAPI clients identify this attribute.
isDefunct
A boolean value that indicates whether the attribute is defunct. See Disabling Existing Classes and Attributes.


classSchema Object Properties

An object's type is defined by a schemaClass object in the Schema container. A schemaClass object has the following key properties:

cn (Common-Name)
The name of the object class. The Common-Name property is unique across all Common-Names for all schemaClass and schemaAttribute objects in the Schema container.
lDAPDisplayName (LDAP-Display-Name)
The name used by LDAP clients to read and write the class using the LDAP protocol. For information on composing a common-name and an LDAP display name for a new class, see Naming Attributes and Classes.

The LDAP-Display-Name property is unique across all LDAP-Display-Names for all schemaClass and schemaAttribute objects in the Schema container.

schemaIDGUID (Schema-ID-GUID)
A GUID stored as an octet string. This GUID uniquely identifies the class. This GUID can be used in access control entries to control access to objects of this class.

On creation of the classSchema object, Active Directory generates this value if it is not specified. If you are creating a new class, it is recommended that you generate your own GUID for each class so that all installations of your extension will use the same schemaIDGUID to refer to the class.

objectGUID (Object-GUID)
A single-valued property that is the unique identifier for the object. This property is a GUID (Globally Unique Identifier). When an object is created in the directory, Active Directory generates a GUID and assigns it to the object's objectGUID property. The GUID is unique across the enterprise and anywhere else.

The objectGUID is a 128-bit GUID structure stored as an OctetString.

adminDisplayName
The display name of the class if the classDisplayName of the class's display specifier is not set or if the class's display specifier does not exist. By default, this value is the same as the cn value.
governsID
The OID of the class. This value must be unique among the attributeIDs of all attributeSchema objects and governsIDs of all classSchema objects. See the section on OIDs.
objectClassCategory
An integer value that specifies the category of the class. The category can be Structural, Abstract, or Auxiliary. The values are based on the following enumeration:
typedef enum {
    ADS_CLASS_CATEGORY_STRUCTURAL = 0x1,
    ADS_CLASS_CATEGORY_ABSTRACT   = 0x2,
    ADS_CLASS_CATEGORY_AUXILIARY  = 0x3
} ADS_CLASS_CATEGORY_ENUM;
possSuperiors
A multi-valued property that specifies the structural classes that can be legal parents of instances of this class. For an existing classSchema object, values can be added to this property but not removed.

Note that the full set of possible superiors is the union of the systemPossSuperiors and possSuperiors on this class as well as the systemPossSuperiors and possSuperiors properties of all inherited superclasses (structural or abstract classes). Note that possSuperiors are not inherited from auxiliary classes.

systemPossSuperiors
A multi-valued property that specifies the structural classes that can be legal parents of instances of this class. After creation of the class, this property cannot be changed.

Note that the full set of possible superiors is the union of the systemPossSuperiors and possSuperiors on this class as well as the systemPossSuperiors and possSuperiors properties of all inherited superclasses (structural or abstract classes). Note that possSuperiors are not inherited from auxiliary classes.

auxiliaryClass
A multi-valued property that specifies the auxiliary classes that this class inherits from. For an existing classSchema object, values can be added to this property but not removed.

Note that the full set of auxiliary classes is the union of the systemauxiliaryClass and auxiliaryClass on this class as well as the systemauxiliaryClass and auxiliaryClass properties of all inherited classes.

systemAuxiliaryClass
A multi-valued property that specifies the auxiliary classes that this class inherits from. After creation of the class, this property cannot be changed.

Note that the full set of auxiliary classes that this class inherits from is the union of the systemauxiliaryClass and auxiliaryClass on this class as well as the systemauxiliaryClass and auxiliaryClass properties of all inherited classes.

subClassOf
Specifies the class from which this class is derived.

For structural classes, the subClassOf can be a structural or abstract class.

For abstract classes, the subClassOf can only be an abstract class.

For auxiliary classes, the subClassOf can be an abstract or auxiliary class.

The value is the lDAPDisplayName of a class. Note that you must ensure that the class exists or will exist when the new class is written to the directory. If class does not exist, the classSchema object will fail to be added to the directory.

description
A text description of the class for documentation purposes.
defaultObjectCategory
The distinguished name of the classSchema object for the class that should be used by default as the objectCategory for new instances of this class. This is an indexed property used to make object class searches fast and efficient.

By default, defaultObjectCategory is set to the DN of the classSchema object for this class. If this object will be frequently queried by the value of a super class rather than the object's own class, the defaultObjectCategory can be defined in the schema to the desired value.

If you are sub-classing a Category 1 class, best practice is to set this to the same value as the super class. This allows the standard UI to "find" your sub-class.

defaultHidingValue
Specifies the default setting of the showInAdvancedViewOnly property of new instances of this class.

Many directory objects are not interesting to end users. To keep these objects from cluttering the UI, every object has a boolean attribute called showInAdvancedViewOnly.

If this value is set to TRUE, it means that instances of the object are hidden in the Administrative snap-ins and the Windows shell. It also means that a menu item for the new object class will not appear in the New context menu of the Administrative snap-ins — even if the appropriate creation wizard properties are set on the new object class's displaySpecifier object.

If this value is set to FALSE, it means that instances of the object are displayed in the Administrative snap-ins and the Windows shell. Set this property to FALSE if you want to be able to see instances of the class in the administrative snap-ins and the shell and enable a creation wizard and its menu item in the New menu of the administrative snap-ins.

If the defaultHidingValue value is not set, the default is TRUE.

systemFlags
An integer value that contains flags define additional properties of the class such as whether class is category 1 or 2. Use the ADS_SCHEMA_SYSTEMFLAG enumeration to decipher the flags.
systemOnly
A boolean value that specifies whether only Active Directory can modify the class.
mustContain
A multi-valued property that specifies the attributes that must be present on instances of this class. These are mandatory attributes that must be present during creation and cannot be cleared after creation. For an existing classSchema object, values can be added to this property but not removed.

Note that the full set of mandatory attributes for this class is the union of the systemMustContain and mustContain on this class as well as the systemMustContain and mustContain properties of all inherited classes.

systemMustContain
A multi-valued property that specifies the attributes that must be present on instances of this class. These are mandatory attributes that must be present during creation and cannot be cleared after creation. After creation of the class, this property cannot be changed.

Note that the full set of mandatory attributes for this class is the union of the systemMustContain and mustContain on this class as well as the systemMustContain and mustContain properties of all inherited classes.

mayContain
A multi-valued property that specifies the attributes that may be present on instances of this class. These are optional attributes that are not mandatory and, therefore, may or may not be present on an instance of this class. For an existing classSchema object, values can be added to this property but not removed.

Note that the full set of optional attributes for this class is the union of the systemMayContain and mayContain on this class as well as the systemMayContain and mayContain properties of all inherited classes.

systemMayContain
A multi-valued property that specifies the attributes that may be present on instances of this class. These are optional attributes that are not mandatory and, therefore, may or may not be present on an instance of this class. After creation of the class, this property cannot be changed.

Note that the full set of optional attributes for this class is the union of the systemMayContain and mayContain on this class as well as the systemMayContain and mayContain properties of all inherited classes.

rDnAttId
Specifies the name of the attribute that will be the naming attribute for the new class — if different than the default ("cn").

Use of a naming attribute other than "cn" is discouraged. Naming attributes should be drawn from the well-known set (OU, CN, O, L and DC) that is understood by all LDAP version 3 clients.

defaultSecurityDescriptor
Specifies the default security descriptor for new objects of this class. This security descriptor is used to provide default protection on the object if there is no security descriptor specified during the creation of the object, and if there are no object-specific inherited permissions from the parent.

The defaultSecurityDescriptor is specified in a special string format defined in a security descriptor definition language (SDDL) syntax. This syntax is described in Microsoft® Software Developer's Kit documentation.

For more information on the default security descriptor, see the Security chapter.

isDefunct
A boolean value that indicates whether the class is defunct. See Disabling Existing Classes and Attributes.


Syntaxes

The syntax of an attribute is defined by the combination of the values set on its attributeSchema object's attributeSyntax and oMSyntax properties. These values must be provided when the attribute is created.

The following table contains the set of the syntaxes supported for Active Directory.

Name | oMSyntax | attributeSyntax | Description
Boolean 1 2.5.5.8 Boolean.

For queries that include attributes of Boolean syntax in a filter, you must specify TRUE or FALSE (for example, myboolattr=TRUE).
Integer | 2 | 2.5.5.9 | 32-bit integer.
OctetString 4 2.5.5.10 Array of bytes.

Use OctetString to store binary data.
INTEGER8 | 65 | 2.5.5.16 | Large Integer. Use for 64-bit values.
ObjectSecurityDescriptor | 66 | 2.5.5.15 | Octet string containing a security descriptor.
DN 127 2.5.5.1 String containing a distinguished name (DN).

The oMObjectClass is the following value:

\x2B0C0287731C00854A

Use this string syntax to store distinguished names that you want kept up-to-date by Active Directory. When an attribute of DN syntax is created with a valid DN, Active Directory treats the attribute as a reference to the object represented by the DN that was set. If the referenced object is renamed or moved, Active Directory ensures that the attribute reflects the change. If the attribute is reset with a new DN, the attribute is reference to the object represented by the new DN.

This string syntax can also be used for linked DNs. Back links must be of syntax DN. Forward links can be of syntax DN (as well as DNWithString, DNWithBinary, AccessPointDN, or ORName. Linked attributes must have a linkID defined. See the description of linkID in attributeSchema properties.

For queries that include attributes of DN syntax in a filter, you must specify full distinguished names—wildcards (for example, cn=foo*) are not supported for filters on attributes with DN syntax.
DNWithOctetString 127 2.5.5.7 OctetString containing a binary value and a distinguished name (DN). Active Directory keeps the DN up-to-date.

The oMObjectClass is the following value:

\x2A864886F7140101010B

DNWithOctetString has the following format:

B:CharCount:binaryvalue:ObjectDN

where

CharCount

The count of hex digits in binaryvalue.

binaryvalue

The hex digit representation of the binary value stored.

ObjectDN

The distinguished name of an object referenced by this attribute. Active Directory maintains the ObjectDN portion so that it contains the current DN of the object originally specified when the value was created.

For more information about how DNWithOctetString is used, see the Enabling Rename-safe Binding with the otherWellKnownObjects Property.
ORName 127 2.5.5.7 From X400

The oMObjectClass is the following value:

\x56060102050B1D
CaseExactString 27 2.5.5.3 String: General-String character set, case-sensitive.

Use this string syntax for attributes that should contain only characters from the General-String character set.

Active Directory does not currently enforce the General-String character set restriction on attributes of CaseExactString syntax; however, future versions may enforce it. Therefore, if you use attributes of syntax CaseExactString, you should only use characters in the General-String character set.
DirectoryString 64 2.5.5.12 String: Unicode, case-insensitive.

Use this string syntax if the attribute must be capable of containing strings supporting the full range of the Unicode character set and if the attribute does not require case-sensitivity. When Active Directory performs comparisons against attributes of this syntax (such as evaluating a query), it performs case-insensitive comparisons.
GeneralizedTime 24 2.5.5.11 Generalized-Time is time string format defined by ASN.1 standards. See ISO 8601 and X680.

Use this syntax for storing time values in Generalized-Time format.
IA5String 22 2.5.5.5 String: IA5 character set, case-sensitive.

Use this string syntax for attributes that should contain only characters from the IA5 character set.

Active Directory does not currently enforce the IA5 character set restriction on attributes of IA5String syntax; however, future versions may enforce it. Therefore, if you use attributes of syntax IA5String, you should only use characters in the IA5 character set.
NumericString 18 2.5.5.6 String containing digits.

Active Directory does not currently enforce the digit restriction on attributes of NumericString syntax; however, future versions may enforce it. Therefore, if you use attributes of syntax NumericString, you should only use digits.
OID | 6 | 2.5.5.2 | String for containing OIDs. The OID is a string containing digits (0-9) and decimal points (.).
UTCTime 23 2.5.5.11 UTC-Time is time string format defined by ASN.1 standards. See ISO 8601 and X680.

Use this syntax for storing time values in UTC-Time format.
CaseIgnoreString 20 2.5.5.4 String: teletex character set, case-insensitive.

Use this string syntax for attributes that should contain only characters from the teletex character set.

Active Directory does not currently enforce the teletex character set restriction on attributes of CaseIgnoreString syntax; however, future versions may enforce it. Therefore, if you use attributes of syntax CaseIgnoreString, you should only use characters in the teletex character set.
AccessPointDN 127 2.5.5.14 From X400

The oMObjectClass is the following value:

\x2B0C0287731C00853E

PresentationAddress 127 2.5.5.13 String for containing OSI presentation addresses.

The oMObjectClass is the following value:

\x2B0C0287731C00855C

See RFC 2252 and 1278
PrintableString 19 2.5.5.5 String: Printable character set, case-sensitive.

Use this string syntax for attributes that should contain only characters from the Printable character set.

Active Directory does not currently enforce the Printable character set restriction on attributes of PrintableString syntax; however, future versions may enforce it. Therefore, if you use attributes of syntax PrintableString, you should only use characters in the Printable character set.
DNWithString 127 2.5.5.14 OctetString containing a string value and a distinguished name (DN). Active Directory keeps the DN up-to-date.

The oMObjectClass is the following value:

\x2A864886F7140101010C

DNWithString has the following format:

S:CharCount:stringvalue:ObjectDN

where

CharCount

The count of characters in stringvalue.

stringvalue

The string value stored.

ObjectDN

The distinguished name of an object referenced by this attribute. Active Directory maintains the ObjectDN portion so that it contains the current DN of the object originally specified when the value was created.

Enumeration | 10 | 2.5.5.9 | Defined by the ITU. Treated as an integer in Active Directory.
ReplicaLink 127 2.5.5.10 System only. Used by Active Directory.

The oMObjectClass is the following value:

\x2A864886F71401010106

If the attribute needs to store a binary value, use OctetString. If the attribute needs to store a linked DN, use DN, DNWithString, or DNWithBinary depending on your needs.
Sid | 4 | 2.5.5.17 | Octet string containing a security identifier (SID). Use this syntax to store SID values only.


What You Must Know Before Extending the Schema

Before you extend the schema, you should consider the following:

  • Impact of Schema Changes.
  • When to Extend the Schema.
  • Restrictions on Schema Extension.


Impact of Schema Changes

A schema extension has the following impact on an Active Directory™ forest:

  • Schema changes are global. There is a single schema for an entire forest. The schema is globally replicated: a copy of the schema exists on every DC in the forest. When you extend the schema, you do so for the entire forest.
  • Schema object additions are not reversible. Once a new class or attribute object has been added to the schema, it cannot be removed. An existing attribute or class can be disabled, but not removed (see Disabling Existing Classes and Attributes). Disabling a class or attribute does not affect existing instances of the class or attribute, but it prevents new instances from being created. You cannot disable an attribute if it is included in any class that is not disabled.
  • OIDs must be unique. Once an attribute or class has been added to the schema, no attribute or class with the same OID can be added. This is true even if a class or attribute has been disabled. For this reason it is very important that you use valid OIDs. Do not "make up" an OID, and do not "recycle" an OID that you have previously used. Valid OIDs are easily obtained, see Obtaining a Root Object Identifier (OID) for details.
  • Some changes can be made after creation but are not reversible. The following schema changes can be made after creation but are not reversible (that is, you can add them but you cannot remove them):
    • Addition of a new class to the list of possSuperiors properties for another class
    • Addition of a new attribute to the list of mayContain properties for another class
  • The lDAPDisplayName for a Category 2 class or attribute can be changed after it has been created. Typically, you should not change the lDAPDisplayName. However, there is one legitimate reason for changing the LDAP name and that is if you made a mistake in defining the attribute or class and want to create a new one to replace the old one. You do not have to rename the relative distinguished name (RDN) of the attribute or class schema to achieve this. Changing the LDAP name enables you to work around a mistake rather than rebuilding your whole Windows 2000 infrastructure or living with the mistake forever. For more information, see Disabling Existing Classes and Attributes.

    The lDAPDisplayName for Category 1 classes or attributes cannot be changed. For information on Category 1 and 2 schema objects, see Restrictions on Schema Extension.


When to Extend the Schema

Extend the schema only if no existing object class meets your needs. Extending the schema is a "heavy weight" operation; schema changes are replicated to every Domain Controller in the enterprise. Consider your needs carefully and assess whether an extension is really necessary.

The schema can be extended in three ways:

  1. Derive a new subclass from an existing class - the subclass has all of the attributes of the superclass and any additional attributes you specify. This is the preferred mechanism for adding attributes to an existing class.
  2. Create an entirely new class with the attributes you want.
  3. Add additional attributes to an existing class and/or add additional possible parents for the class

Derive from an existing class when:

  • The existing class needs additional attributes, but otherwise meets your needs.
  • You don't require the ability to transform existing objects of the class into your new class. It is not possible to "add a subclass" to an existing object.
  • You want to use the existing Directory Manager snap-in to manage the extended attributes of the objects.

Modification of an existing class is discouraged. required when your application requires the ability to extend existing objects of the class. For instance, to add application-specific information to the User object, you'd normally extend the class User, because you need to deal with existing Users not just special Users created by your application.

Whether you subclass an existing class or modify an existing class, you will want to extend tools like the Active Directory Users and Computers snap-in to manage the extended attributes of the objects.

Create a completely new class (that is, a class derived from "Top") when no existing class meets your needs.


Restrictions on Schema Extension

In order to reduce the possibility of schema changes by one application breaking other applications and to maintain schema consistency, Active Directory enforces restrictions on the type of schema changes that an application or user is allowed to make.

The restrictions are imposed only on modification of existing schema objects. The schema is categorized into two categories. The schema objects that ship with Windows 2000 in the base schema belong to Category 1. Any schema objects added later by other applications or users through dynamic schema extension belong to Category 2. The category of a schema object can be determined by the 0x10 bit set in the systemFlags attribute on the schema object. This bit is defined as ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT in the ADS_SYSTEMFLAG_ENUM enumeration. This bit is only set on Category 1 objects, and cannot be altered, nor can it be set on any Category 2 object.

The systemFlags attribute is used internally by Active Directory to identify special characteristics of "infrastructure" objects in the base schema. In addition to identifying Category 1 objects, systemFlags controls whether an object can be moved, deleted, or renamed. These operations are prevented for objects that Windows 2000 depends on to run.

Restrictions on all schema objects

On any schema objects, Category 1 or 2, Active Directory imposes the following restrictions:

  • You cannot add a new mustContain to a class (directly or through inheritance by adding an auxiliary class)
  • You cannot delete any mustContain, mayContain, or possSuperiors of the class (directly or through inheritance).

Additional restrictions on Category 1 objects

In addition, the following additional restrictions are imposed on Category 1 schema objects, since many Windows 2000® components depend on the following:

  • You cannot change the following attributes of a Category 1 attribute:
    1. rangeLower and rangeUpper (value range)
    2. attributeSecurityGuid (determines which property set the attribute belongs in, if any)
  • You cannot change the defaultObjectCategory of a Category 1 class.
  • You cannot change the objectCategory of any instance of a Category 1 class.
  • You cannot make a Category 1 class or attribute defunct.
  • You cannot change the lDAPDisplayName of a Category 1 class or attribute.
  • You cannot rename a Category 1 class or attribute.


Querying for Category 1 or 2 Schema Objects

The systemFlags property of attributeSchema and classSchema objects is an integer that is a bit-mask containing flags for additional system qualities that the attribute or class possesses. The category of an attribute or class is specified by the ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT. If this flag is set, the attribute or class is category 1. If it is not set, it is category 2. If no systemFlags property exists on the attributeSchema or classSchema object, it is category 2.

The following enumeration (defined in IADS.H) defines the available flags:

enum
    {   ADS_SYSTEMFLAG_DISALLOW_DELETE  = 0x80000000,
        ADS_SYSTEMFLAG_CONFIG_ALLOW_RENAME      = 0x40000000,
        ADS_SYSTEMFLAG_CONFIG_ALLOW_MOVE        = 0x20000000,
        ADS_SYSTEMFLAG_CONFIG_ALLOW_LIMITED_MOVE        = 0x10000000,
        ADS_SYSTEMFLAG_DOMAIN_DISALLOW_RENAME   = 0x8000000,
        ADS_SYSTEMFLAG_DOMAIN_DISALLOW_MOVE     = 0x4000000,
        ADS_SYSTEMFLAG_CR_NTDS_NC       = 0x1,
        ADS_SYSTEMFLAG_CR_NTDS_DOMAIN   = 0x2,
        ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED      = 0x1,
        ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED      = 0x4,
        ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT 0x10
    }   ADS_SYSTEMFLAG_ENUM;

To query for objects based on a bit set in a property, you must use a filter that contains a matching rule.

Matching rules have the following syntax:

attibutename:ruleOID:=value

where attributename is the lDAPDisplayName of the attribute, ruleOID is the OID for the matching rule, and value is the value you want to use for comparison. Note that value must be a decimal value (not hexidecimal).

Active Directory supports two matching rules.

Matching Rule OID | Description
1.2.840.113556.1.4.803 | LDAP_MATCHING_RULE_BIT_AND The matching rule is true when all bits from the property that correspond to 1-bits in the value are 1.
1.2.840.113556.1.4.804 | LDAP_MATCHING_RULE_BIT_OR The matching rule is true when any bits from the property match that correspond to 1-bits in the value are 1.

These two matching rules are only different when the value is a bit mask with multiple bits set. For example, the following queries will return the same result because they contain a single bit mask in the value — the query returns all attributeSchema objects with the ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED bit flag set.

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.803: = 1) )

and

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804: = 1) )

The following queries will return different results because the value is a bit mask with more than one flag set (ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED|ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT is 1 + 16 = 17).

The first one uses the bit AND operator so attributeSchema objects with both the ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED and ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT flags set are returned:

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.803: = 17) )

The second one uses the bit OR operator so the union of attributeSchema objects with the ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED set and the attributeSchema objects with the ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT flag set is returned:

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804: = 17) )

To look for an exact match of a value, simply use the equals operator. The following example returns attributeSchema objects that have only the ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED and ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT flags set:

(&(objectCategory=attributeSchema)(systemFlags=17))

Querying for Category 1

The following query string searches for category 1 attributes (attributeSchema objects with the ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT flag set in the systemFlags property):

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804: = 16) )

Note in the example above that the LDAP query syntax takes decimal values; therefore, the hex value of the flag must be converted to decimal. In this case, ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT is 0x10 so the filter value must be specified as 16.

Querying for Category 2

The following query string searches for category 2 attributes (attributeSchema objects that do not have the ADS_SYSTEMFLAG_SCHEMA_BASE_OBJECT flag set in the systemFlags property):

(&(objectCategory=attributeSchema)(!(systemFlags:1.2.840.113556.1.4.804: = 16)))

Note that this query will also return attributeSchema objects that do not have a systemFlags property (and, therefore, implicitly do not have the specified flag set).


How to Extend the Schema

Once you have decided that schema extension is required, do the following:

  1. Name the new attributes and classes
  2. Obtain OIDs for new attributes and classes
  3. Use display specifiers to integrate new attributes and classes with the user interface, if necessary.
  4. Create a program or script (LDIF or CSV) to install your schema extensions (create new attributes and classes or add attributes to existing classes).


Naming Attributes and Classes

Adhere to the following recommendations with respect to schema naming for both the RDN attribute, which is common-name (cn), and the LDAP display name (lDAPDisplayName).

Common-Name (cn)

Use the following rules to compose a common-name.

  • The first section is a company prefix; for example, MS for "Microsoft". This ensures that all attributes and classes for a specific company are displayed consecutively when browsing the schema. Choose the company prefix carefully to minimize the possibility of collisions. For publicly-traded companies, the company's ticker symbol is a good prefix. For non-publicly-traded companies, the prefix could be the registered DNS domain name or an abbreviation thereof.
  • The next character should be a hyphen "-". Use hyphens to separate each section in the name.
  • The next section succinctly identifies the product. Begin with an upper-case letter followed by the remainder of the product-specific section in whatever case is appropriate. It's acceptable to use all upper-case letters for this section if the product identifier is an acronym.
  • The remaining sections describe the attribute or class. This part of the common-name should be descriptive. Don't use cryptic names that are meaningless to developers and viewers of the schema.

LDAP-Display-Name (lDAPDisplayName)

You should explicitly specify the lDAPDisplayName for all of your attributes and classes. Use the following rules to derive an lDAPDisplayName from a common-name.

  • Make the first character lowercase.
  • Capitalize the first character immediately following each hyphen ("-").
  • Remove all hyphens except those immediately following the company and product components of the name.

    The following table illustrates the naming rules as applied to both common-name (cn) and LDAP-Display-Name (lDAPDisplayName).

Common-Name | LDAP-Display-Name
MS-MQ-Enterprise-Settings | mS-MQ-EnterpriseSettings
MS-Exch-Connection-Agreement | mS-Exch-ConnectionAgreement
MS-DS-Consistency-GUID | mS-DS-ConsistencyGUID
MS-RRAS-Attribute | mS-RRAS-Attribute

If you don't specify the lDAPDisplayName of an attribute or class, the system automatically uses the common-name to generate one for you. The system uses the following algorithm:

  • Make the first character lowercase
  • Capitalize the first character immediately following each hyphen ("-").
  • Remove all hyphens.

    However, because LDAP display names are case-insensitive, the system's default algorithm can create problems. First, you can't easily tell where the company and product prefixes end and the rest of the name begins without using some reserved character. The "-" character is the only separator available in the LDAP display name syntax. For example, if cn=MS-Exch-Configuration, the auto-generated lDAPDisplayName would be mSExchConfiguration. Another problem with auto-generation of the lDAPDisplayName is that of name collisions. For example, the system's algorithm generates the same case-insensitive lDAPDisplayName for all of the following common-names: MS-Exch-Foo, MSExch-Foo, and MSExchFoo. To avoid these problems, we recommend that you explicitly specify the lDAPDisplayName for all of your attributes and classes.

For development and testing purposes you may wish to append a version suffix to the cn and lDAPDisplayName. In a distributed development/test environment, a version suffix enables developers to run multiple versions of their software simultaneously. After testing has been completed, you can rename your attribute or class to remove the suffix. You cannot delete defunct versions of your schema extensions, but you can disable them and rename them with obscure names, as described in Disabling Existing Classes and Attributes.


Obtaining a Root Object Identifier (OID)

There are two ways to get an OID:

  • Using the ISO Name Registration Authority
  • Using the OIDGEN utility


Obtaining a Root OID from an ISO Name Registration Authority

The preferred means for obtaining a root Object Identifier is to request one from an International Standards Organization (ISO) Name Registration Authority. This is a one-time action; once you have obtained a root OID the OID space it defines is yours and you can administer it yourself.

There is usually a fee associated with registering an organization name and receiving a root OID. Fees and registration policies vary by country. In the United States, the ISO NRA is the American National Standards Institute (ANSI). Information on the ANSI registration procedure and fee schedule can be found at http://web.ansi.org/public/services/reg_org.html. The ISO maintains a list of member organizations at http://www.iso.ch/addresse/membodies.html. If you are outside the United States you should contact the ISO member organization for your country for name registration information.


Using Generated OIDs

The Windows 2000 Resource Kit includes a command line utility, OIDGEN.EXE, that generates valid OID's. The OIDs are generated using a base OID from Microsoft's branch of the ISO OID tree, and a Globally Unique Identifier (GUID) generated each time the program is run.

D:\src>oidgen
Attribute Base OID: 1.2.840.113556.1.4.7000.233.2014652.180672.30.178511.21908.1227698.1827074
    Class Base OID: 1.2.840.113556.1.5.7000.111.2014652.180672.30.178511.21908.1227698.1827075

Note that OIDGEN produces two OIDs: an "Attribute Base OID" and a "Class Base OID". Run OIDGEN once for your organization and then manage the OID space beneath these roots. For example, given the generated OIDs above, the OID for your first new object class would be:

1.2.840.113556.1.5.7000.111.2014652.180672.30.178511.21908.1227698.1827075.1

and the OID for your first new attribute would be:

1.2.840.113556.1.4.7000.233.2014652.180672.30.178511.21908.1227698.1827074.1

and so on. Grouping all of the OIDs for your organization under common roots is required. Active Directory™ maintains an internal table of OIDs; to optimize performance, OIDs are broken into a "prefix" and "suffix". The prefix is the entire OID minus the rightmost (low-order) value. The prefixes are stored in a table so they can be referenced by an index value. The remaining (low-order) part of OID and the index value for its prefix are then used to identify the classes and attributes. Grouping all of your OIDs under common roots keeps the prefix table small. Excessive growth in the prefix table can hurt performance.

To request or register an OID from Microsoft, send e-mail to the following address:

oids@microsoft.com


Integrating Schema Extensions with the User Interface

Active Directory administration tools use a common architecture for connecting the administrative user interface to the directory schema. The centerpiece of this architecture is the displaySpecifier object class. Display specifiers and their usage are described in detail in Extending the User Interface for Directory Objects.

When you create a new class by subclassing an existing class, you can leverage the existing UI for the superclass by copying the superclass' display specifier. An easy way to copy the display specifier for a class is to export it into an LDIF file using LDIFDE, edit the Distinguished name and CN, then import the modified LDIF file. If the subclass has additional attributes, you will need to provide additional property pages to support editing. If the subclass has additional must-have properties, you will need to provide Creation Dialog extension pages since the UI provided by the superclass has no way to create these new attributes.


Defining a New Attribute

Consider the following before defining a new attribute:

  • Use existing attributes wherever possible.
  • Always use "common name" (cn) as the naming (RDN) attribute — this is the default for most classes, including those derived directly from Top. CN is an indexed attribute and will make searching for your object by name efficient.
  • Large multi-valued attributes are costly to store and retrieve and should be avoided. Active Directory implements an LDAP extension to allow incremental read of large properties with multiple values, but not all LDAP clients will know how to use this extension.
  • Remember that attributes are "flat", that is there is no implied substructure to an attribute. All attributes in a given class should relate directly to instances of that class.

To Create a New Attribute

Follow these steps to create a new attribute:

  1. Choose a name for the attribute. For information on composing a common-name and an LDAP display name for a new attribute, see Naming Attributes and Classes.
  2. Obtain an OID for the attribute. See Obtaining a Root Object Identifier (OID).
  3. Choose a syntax for the attribute The syntax is determined by the combination of oMSyntax and attributeSyntax. See Choosing a Syntax.
  4. Decide if the attribute is single or multi-valued
  5. Decide if the attribute should be indexed by default.
  6. Decide if the attribute should be in the global catalog by default.
  7. If the attribute is an integer or string, decide if a range limit is needed
  8. If the attribute is DN-valued, decide if the attribute should be linked with another attribute. If so, the linkID property must be set appropriately on each attribute (one attribute must be a forward link, the other a back link). The schema extension program must use an available set of link IDs. See sample code for the Finding the Next Link ID example below.
  9. Create a new attributeSchema object. There are a large number of attributes that can be set for an attributeSchema object. Those listed below are the ones critical to the definition of a new attribute.

    Set the following attributes:

    cn (Common-Name)
    The name of the attribute. Usually, the Common-Name contains no spaces with words separated by hyphens. The Common-Name property is unique across all Common-Names for all schemaClass and schemaAttribute objects in the Schema container. So use the same naming prefix as you use for LDAP display name, to ensure uniqueness.
    lDAPDisplayName
    The name used by LDAP clients to read and write the attribute using the LDAP protocol. The lDAPDisplayName property must be unique across all lDAPDisplayName attributes for all schemaClass and schemaAttribute objects in the Schema container.
    attributeID
    The OID you obtained for the attribute. This value must be unique among the attributeIDs of all attributeSchema objects and governsIDs of all classSchema objects. See the section on Object Identifiers (OIDs).
    attributeSyntax
    The OID for the syntax of the attribute. See the table in Syntaxes.
    oMSyntax
    The OM syntax of the attribute. The combination of oMSyntax and AttributeSyntax are taken together to define the syntax.
    oMObjectClass
    An octet string that must be specified for attributes of oMSyntax 127. The oMObjectClass is not used for attributes with other oMSyntaxes.
    schemaIDGUID
    A GUID that uniquely identifies the attribute. It is recommended that you generate your own GUID for each attribute so that all installations of your schema extension will use the same schemaIDGUID to refer to the attribute. If you no value is specified, Active Directory generates a GUID.
    isSingleValued
    A Boolean value that specifies whether the attribute is single-valued (TRUE) or multi-valued (FALSE). Default is FALSE if this value is not set.
    searchFlags
    An integer value that contains bit flags. The attribute is indexed if the least significant bit is set to 1, or non-indexed if the bit is zero.
    isMemberOfPartialAttributeSet
    A boolean value that defines whether the attribute is replicated to the global catalog (in the global catalog has a value of TRUE, not in the global catalog is FALSE).
    rangeLower
    Optional. An integer that specifies the lower value of the range of values for this attribute. All values set for this attribute must be greater than or equal to this value. If both rangeLower and rangeUpper are set, rangeLower must be less than rangeUpper. For integers, value means the value of the integer. For string syntaxes, value means the number of characters in the string. For octet strings, value means the number of bytes.
    rangeUpper
    Optional. An integer that specifies the upper value of the range of values for this attribute. All values set for this attribute must be less than or equal to this value. If both rangeLower and rangeUpper are set, rangeLower must be less than rangeUpper. For integers, value means the value of the integer. For string syntaxes, value means the number of characters in the string. For octet strings, value means the number of bytes.
    linkID
    Optional. Required for linked attributes. The linkID is an integer that indicates that the attribute is a linked attribute. An even integer is a forward link and an odd integer is a back link.

    This value must be unique for linkIDs of all attributeSchema objects. A back link must have corresponding forward link. See Example Code for Finding the Next linkID.

    Note that the corresponding forward link must exist before a back link attribute can be created.

    A forward-link/back-link pair is identified by attributes with linkID x and x+1 where x > 1 and is an even integer.

    Forward links must have attributeSyntax value 2.5.5.1, 2.5.5.7, or 2.5.5.14.

    Back links can have attributeSyntax 2.5.5.1 only.

    adminDisplayName
    The display name of the attribute if a mapping does not exist in the attributeDisplayNames property of the display specifier for the class that contains this attribute. If you do not set this value, it is the same as the cn value.
    description
    Optional. A text description of the attribute.
  10. Write the new attributeSchema object to the directory.
  11. Update the schema cache if necessary.


Example Code for Creating an Attribute

The following functions create an attributeSchema object in the ADSI cache.

The CreateAttribute function creates the attributeSchema object. Note that the IADs::SetInfo must be called in order to commit the new attributeSchema object to the directory.

The BytesToVariantArray function is a utility function that packs an octet string into a variant array.

C/C++

//Create an attribute with IADsContainer::Create. There is caching.
//This function creates the attributeSchema object but does not commit it.
//It is up to the caller to call IADs::SetInfo on the new attribute object
//and commit it to the ds.
HRESULT CreateAttribute(
    IADsContainer *pSchema, //IADsContainer to schema container.
    LPOLESTR szAttributeName,
    LPOLESTR szLDAPDisplayName,//
    LPOLESTR szAttributeOID,
    const GUID * pSchemaIDGUID,//
    LPOLESTR szAttributeSyntax,
    int iOmSyntax,
    PBYTE pbomObjectClass,
    DWORD dwSizeomObjectClass, //size of pbomObjectClass.
    LPOLESTR szDescription,
    BOOL bIsSingleValued,
    BOOL bIsInGC,
    BOOL bIsIndexed,
    int iLowerRange,//
    int iUpperRange,//
    int iLinkID,//
    LPOLESTR szAdminDisplayName,//
    IADs **ppNewAttribute //Return an IADs pointer to the new attribute.
    )
{
if ((!szAttributeName)
    ||(!szAttributeOID)
    ||(!szAttributeSyntax)
    ||(!iOmSyntax)
    ||(!szAttributeSyntax)
   )
  return E_INVALIDARG;
if (!ppNewAttribute)
  return E_POINTER;
HRESULT hr = E_FAIL;
HRESULT hrPut = S_OK;
IDispatch *pDisp = NULL;
LPOLESTR szBuffer = new OLECHAR[MAX_PATH*2];
LPOLESTR szAttributeToSet = NULL;
wcscpy(szBuffer,L"cn=");
wcscat(szBuffer,szAttributeName);
(*ppNewAttribute) = NULL;
VARIANT var;
if (!pSchema)
  return E_POINTER;
hr = pSchema->Create(L"attributeSchema",szBuffer,&pDisp);
if (SUCCEEDED(hr))
{
  hr = pDisp->QueryInterface(IID_IADs, (void**)ppNewAttribute);
  while (SUCCEEDED(hr))
  {
    //Put cn Put this value so that it can be read from the cache.
    szAttributeToSet = L"cn";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szAttributeName);
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put lDAPDisplayName.
    //If NULL, let it default (ie, don't set it).
    if (szLDAPDisplayName)
    {
      szAttributeToSet = L"lDAPDisplayName";
      var.vt = VT_BSTR;
      var.bstrVal = SysAllocString(szLDAPDisplayName);
      hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    //Put attributeID
    szAttributeToSet = L"attributeID";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szAttributeOID);
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put schemaIDGUID.
    //If NULL, let it default (ie, don't set it).
    if (pSchemaIDGUID)
    {
      hrPut = BytesToVariantArray(
        (LPBYTE)pSchemaIDGUID, //Pointer to bytes to put in a variant array.
        sizeof(GUID),//Size of pValue in bytes.
        &var //Return variant containing octet string (VT_UI1|VT_ARRAY).
        );
      if (SUCCEEDED(hrPut))
      {
       szAttributeToSet = L"schemaIDGUID";
      hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
      VariantClear(&var);
      }
      hr = hrPut;
    }

    //Put attributeSyntax
    szAttributeToSet = L"attributeSyntax";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szAttributeSyntax);
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put oMSyntax
    szAttributeToSet = L"oMSyntax";
    var.vt = VT_I4;
    var.lVal = iOmSyntax;
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put searchFlags
    szAttributeToSet = L"searchFlags";
    var.vt = VT_I4;
    if (bIsIndexed)
       var.lVal = 1;
    else
       var.lVal = 0;
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put isSingleValued
    szAttributeToSet = L"isSingleValued";
    var.vt = VT_BOOL;
    if (bIsSingleValued)
      var.boolVal = VARIANT_TRUE;
    else
      var.boolVal = VARIANT_FALSE;
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put isMemberOfPartialAttributeSet
    szAttributeToSet = L"isMemberOfPartialAttributeSet";
    var.vt = VT_BOOL;
        if (bIsInGC)
      var.boolVal = VARIANT_TRUE;
    else
      var.boolVal = VARIANT_FALSE;
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put description
    szAttributeToSet = L"description";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szDescription);
    hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put rangeLower and rangeUpper
    //If both are 0, let them default (ie, don't set them).
    if ((iLowerRange>=0)&&(iUpperRange>0))
    {
      //Make sure they are legal values
      if (iLowerRange < iUpperRange)
      {
        //Set rangeUpper
        szAttributeToSet = L"rangeUpper";
        var.vt = VT_I4;
        var.lVal = iUpperRange;
        hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
        VariantClear(&var);
        if (SUCCEEDED(hrPut))
        {
          //Set rangeLower
          szAttributeToSet = L"rangeLower";
          var.vt = VT_I4;
          var.lVal = iLowerRange;
          hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
          VariantClear(&var);
        }
        hr = hrPut;
      }
      else
        hr = E_INVALIDARG;
    }

    //Put linkID. If linkID is 0, let it default (ie, don't set it).
    if (iLinkID>0)
    {
      szAttributeToSet = L"linkID";
      var.vt = VT_I4;
      var.lVal = iLinkID;
      hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    //Put adminDisplayName.
    //If NULL, set it to the same string as cn.

      szAttributeToSet = L"adminDisplayName";
      var.vt = VT_BSTR;
      if (!szAdminDisplayName)
      {
        var.bstrVal = SysAllocString(szAttributeName);
      }
      else
      {
        var.bstrVal = SysAllocString(szAdminDisplayName);
      }
      hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;

    //Put omObjectClass
    //Should only set this if it is meant to be a disambiguator for omSyntax 127 attributes.
    if (pbomObjectClass && dwSizeomObjectClass)
    {
      hrPut = BytesToVariantArray(
        pbomObjectClass, //Pointer to bytes to put in a variant array.
        dwSizeomObjectClass,//Size of pValue in bytes.
        &var //Return variant containing octet string (VT_UI1|VT_ARRAY).
        );
      if (SUCCEEDED(hrPut))
      {
       szAttributeToSet = L"omObjectClass";
      hrPut = (*ppNewAttribute)->Put(szAttributeToSet,var);
      VariantClear(&var);
      }
      hr = hrPut;
    }
    //End of properties to set.
    break;
  }
}
if (pDisp)
  pDisp->Release();

if (FAILED(hr))
{
  //Clean up if failed.
  if (*ppNewAttribute)
  {
    (*ppNewAttribute)->Release();
    (*ppNewAttribute) = NULL;
  }

}

return hr;
}

HRESULT BytesToVariantArray(
    PBYTE pValue, //Pointer to bytes to put in a variant array.
    ULONG cValueElements,//Size of pValue in bytes.
    VARIANT *pVariant //Return variant containing octet string (VT_UI1|VT_ARRAY).
    )
{
    HRESULT hr = E_FAIL;
    SAFEARRAY *pArrayVal = NULL;
    SAFEARRAYBOUND arrayBound;
    CHAR HUGEP *pArray = NULL;

    //Set bound for array
    arrayBound.lLbound = 0;
    arrayBound.cElements = cValueElements;

    //Create the safe array for the octet string. unsigned char elements;single dimension;aBound size.
    pArrayVal = SafeArrayCreate( VT_UI1, 1, &arrayBound );

    if (!(pArrayVal == NULL) )
    {
        hr = SafeArrayAccessData(pArrayVal, (void HUGEP * FAR *) &pArray );
        if (SUCCEEDED(hr))
        {
            //Copy the bytes to the safe array.
            memcpy( pArray, pValue, arrayBound.cElements );
            SafeArrayUnaccessData( pArrayVal );
            //Set type to array of unsigned char
            V_VT(pVariant) = VT_ARRAY | VT_UI1;
            //Assign the safe array to the array member.
            V_ARRAY(pVariant) = pArrayVal;
            hr = S_OK;
        }
        else
        {
            //Clean up if array can't be accessed.
            if ( pArrayVal )
               SafeArrayDestroy( pArrayVal );
        }
    }
    else
    {
      hr = E_OUTOFMEMORY;
    }

    return hr;
}


Example Code for Finding the Next linkID

The following function finds the largest link ID and returns the next forward link value to use when creating a pair of linked attributes.

C/C++

//Finds the largest link ID and returns the next forward link value to use.
HRESULT GetNextLinkID(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
              int *iNextLinkID
              )
{
  if (!pSchemaNC)
    return E_POINTER;
    //Create search filter
  //Find attributes that have a linkID set.
  LPOLESTR pszSearchFilter = L"(&(objectCategory=attributeSchema)(linkID=*))";

  //Create search preferences
    const DWORD dwNumPrefs = 2;
  ADS_SEARCHPREF_INFO SearchPrefs[dwNumPrefs];
    //Attribute and classes are one-level deep in the Schema container so only need to search one level.
  SearchPrefs[0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
  SearchPrefs[0].vValue.dwType = ADSTYPE_INTEGER;
  SearchPrefs[0].vValue.Integer = ADS_SCOPE_ONELEVEL;

    //Sort order
  //Sort against linkID and sort from highest to lowest value.
  ADS_SORTKEY sortkey[] = { {L"linkID",NULL,FALSE} };
  //Not used set to NULL
  SearchPrefs[1].dwSearchPref = ADS_SEARCHPREF_SORT_ON;
  SearchPrefs[1].vValue.dwType = ADSTYPE_PROV_SPECIFIC;
  SearchPrefs[1].vValue.ProviderSpecific.dwLength = sizeof(sortkey);
  SearchPrefs[1].vValue.ProviderSpecific.lpValue = (LPBYTE)(sortkey);

  // COL for iterations
  LPOLESTR pszColumn = NULL;
  ADS_SEARCH_COLUMN col;
    HRESULT hr;

    // Interface Pointers
    IADs    *pObj = NULL;
    IADs  * pIADs = NULL;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch = NULL;

  // Set the search preference
    hr = pSchemaNC->SetSearchPreference( SearchPrefs, dwNumPrefs);
    if (FAILED(hr))
        return hr;

  LPOLESTR pszReturnedPropertiesList[] = {L"linkID"};

  int iCount = 0;
  DWORD x = 0L;
  //Set largest value to 0
  int iHighestLinkID = 0;

   //Return non-verbose list properties only
    hr = pSchemaNC->ExecuteSearch(pszSearchFilter,
                              pszReturnedPropertiesList,
                  sizeof(pszReturnedPropertiesList)/sizeof(LPOLESTR),
                  &hSearch
                  );
   if ( SUCCEEDED(hr) )
  {
    // Call IDirectorySearch::GetFirstRow() to start the search.
    hr = pSchemaNC->GetFirstRow( hSearch);
    if (SUCCEEDED(hr))
    {
        while( hr != S_ADS_NOMORE_ROWS )
    {
      //Keep track of count.
      iCount++;
            // loop through the array of passed column names,

      while( pSchemaNC->GetNextColumnName( hSearch, &pszColumn ) != S_ADS_NOMORE_COLUMNS )
            {
                hr = pSchemaNC->GetColumn( hSearch, pszColumn, &col );
          if ( SUCCEEDED(hr) )
          {
          if (ADSTYPE_INTEGER==col.dwADsType)
          {
            if (col.pADsValues[x].Integer > iHighestLinkID)
              iHighestLinkID = col.pADsValues[x].Integer;
          }
              pSchemaNC->FreeColumn( &col );
          }
        FreeADsMem( pszColumn );
            }
        //Get the next row
       hr = pSchemaNC->GetNextRow( hSearch);
    }

    }
    // Close the search handle to clean up
      pSchemaNC->CloseSearchHandle(hSearch);
  }
  if (SUCCEEDED(hr) && iCount>0)
  {
    DWORD dwIsOdd;
    (*iNextLinkID) = iHighestLinkID;
    //See if highest link value is divisible by zero.
    dwIsOdd = iHighestLinkID%2;
    //Regardless of even or odd, need to increment once.
    (*iNextLinkID)++;
    //If there is a remainder, value is odd so no need to increment again.

    //If there is no remainder, the value is even
    //so increment next link value again to the next even value.
    if (!dwIsOdd)
      (*iNextLinkID)++;

    hr = S_OK;
  }
  else
  {
    //Search succeeded but could not find any attributes.
    //Should never hit this.
    (*iNextLinkID) = 0;
    hr = S_FALSE;
  }

    return hr;
}


Choosing a Syntax

There are 23 syntaxes defined in Active Directory (see the Syntax section for complete information on the 23 syntaxes). The following table provides a short list of recommendations:

Data to store in attribute | Syntax to use | Comment
Boolean | Boolean | Use for boolean values.
Integer | Integer | Use for integers.
String | DirectoryString | Use for most string attributes. It supports the Unicode character set. When Active Directory performs comparisons against attributes of this syntax (such as evaluating a query), it performs case-insensitive comparisons. Use the other string syntaxes (IA5String, NumericString, and so on) to store strings that should contain only the specific character sets supported by the syntax.
Time | UTCTime | Use this syntax for storing time values in UTC-Time format.
Binary data | OctetString | Use OctetString to store binary data. An OctetString is an array of bytes.
DN Reference | DN | Use to store distinguished names that you want kept up-to-date by Active Directory. When an attribute of DN syntax is created with a valid DN, Active Directory treats the attribute as a reference to the object represented by the DN that was set. If the referenced object is renamed or moved, Active Directory ensures that the attribute reflects the change. If the attribute is reset with a new DN, the attribute is reference to the object represented by the new DN.
Linked DN | DN | This string syntax can be used for linked DNs. Back links must be of syntax DN. Forward links can be of syntax DN (as well as DNWithString, DNWithBinary, AccessPointDN, or ORName. Linked attributes must have a linkID defined. See the description of linkID in attributeSchema properties.
Large Integer (64-bit values) | INTEGER8 | Use for 64-bit values.
Security Descriptor | ObjectSecurityDescriptor | Octet string containing a security descriptor.
Binary data with a DN reference | DNWithOctetString | OctetString containing a binary value and a distinguished name (DN). Active Directory keeps the DN up-to-date.
String data with a DN reference | DNWithString | OctetString containing a string value and a distinguished name (DN). Active Directory keeps the DN up-to-date.
Security Identifier (SID) | Sid | Octet string containing a security identifier (SID). Use this syntax to store SID values only.


Defining a New Class

The DC creating the instance of a class enforces where an object of that class instance can be created. The DC looks at the possSuperiors attribute and ensures that the objectClass of the parent object is in this list. Note that a class also inherits possSuperiors from its superclasses (but not the auxiliary classes).

Be as specific as possible when defining the legal parent classes for the new class. Where do you want users to be able to create instances of your class? For example, specifying "container" as a legal parent will let the user create instances under any of the standard containers (container, organizationalUnit, and so on), while specifying "computer" would allow instances to be created only under instances of the computer object.

To Create a Class

  1. Choose a name for the class. For information on composing a common-name and an LDAP display name for a new class, see Naming Attributes and Classes.
  2. OID obtained for the class.
  3. Choose a "default object category" for the class.
  4. Choose an "object class category" for the class. This indicates whether the class is Abstract, Structural, or Auxiliary.
  5. Create a new classSchema object, setting the selected values in these attributes:
    cn (Common-Name)
    The name of the class. The Common-Name property is unique across all Common-Names for all schemaClass and schemaAttribute objects in the Schema container.
    lDAPDisplayName (LDAP-Display-Name)
    The name used by LDAP clients to read and write the class using the LDAP protocol. The lDAPDisplayName property must be unique across all lDAPDisplayName attributes for all schemaClass and schemaAttribute objects in the Schema container.

    It is recommended that the lDAPDisplayName be the cn with the hyphens removed (except do not remove the hyphen separating your name prefix from the rest of the name) and the first character uses the lower case. The lDAPDisplayName property must be unique across all lDAPDisplayName attributes for all schemaClass and schemaAttribute objects in the schema container.

    It is also recommended that you specify the lDAPDisplayName rather than letting this attribute default.

    schemaIDGUID (Schema-ID-GUID)
    A GUID stored as an octet string. This GUID uniquely identifies the class. This GUID can be used in access control entries to control access to objects of this class.

    On creation of the classSchema object, Active Directory generates this value if it is not specified. If you are creating a new class, it is recommended that you generate your own GUID for each class so that all installations of your extension will use the same schemaIDGUID to refer to the class.

    governsID
    The OID of the class. This value must be unique among the attributeIDs of all attributeSchema objects and governsIDs of all classSchema objects. See the section on OIDs.
    objectClassCategory
    An integer value that specifies the category of the class. The category can be Structural, Abstract, or Auxiliary. The values are based on the following enumeration:
    typedef enum {
    ADS_CLASS_CATEGORY_STRUCTURAL = 0x1,
    ADS_CLASS_CATEGORY_ABSTRACT   = 0x2,
    ADS_CLASS_CATEGORY_AUXILIARY  = 0x3
} ADS_CLASS_CATEGORY_ENUM;
    possSuperiors
    A multi-valued property that specifies the classes that can be legal parents of instances of this class. For an existing classSchema object, values can be added to this property but not removed.

    Each value is the lDAPDisplayName of a class. Note that you must ensure that the classes exist or will exist when the new class is written to the directory. If one of the classes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of possible superiors is the union of the systemPossSuperiors and possSuperiors on this class as well as the systemPossSuperiors and possSuperiors properties of all inherited superclasses (structural or abstract classes). Note that possSuperiors are not inherited from auxiliary classes.

    systemPossSuperiors
    A multi-valued property that specifies the structural classes that can be legal parents of instances of this class. After creation of the class, this property cannot be changed.

    Each value is the lDAPDisplayName of a class. Note that you must ensure that the classes exist or will exist when the new class is written to the directory. If one of the classes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of possible superiors is the union of the systemPossSuperiors and possSuperiors on this class as well as the systemPossSuperiors and possSuperiors properties of all inherited superclasses (structural or abstract classes). Note that possSuperiors are not inherited from auxiliary classes.

    subClassOf
    Specifies the class from which this class is derived.

    For structural classes, the subClassOf can be a structural or abstract class.

    For abstract classes, the subClassOf can only be an abstract class.

    For auxiliary classes, the subClassOf can be an abstract or auxiliary class.

    The value is the lDAPDisplayName of a class. Note that you must ensure that the class exists or will exist when the new class is written to the directory. If class does not exist, the classSchema object will fail to be added to the directory.

    defaultObjectCategory
    The distinguished name of the classSchema object for the class that should be used by default as the objectCategory for new instances of this class. This is an indexed property used to make object class searches fast and efficient.

    By default, defaultObjectCategory is set to the distinguished name of the classSchema object for this class. If this object will be frequently queried by the value of a super class rather than the object's own class, the defaultObjectCategory can be defined in the schema to the desired value.

    If you are sub-classing a structural class, best practice is to set this to the same value as the super class. This allows the standard UI to "find" your sub-class.

    Example:

    //Get the DN for organizationalUnit class to use for the default category .
//m_pSchema is an IADsContainer pointer to the Schema container.
LPOLESTR szDefaultCategory = new OLECHAR[MAX_PATH];
if (m_pSchema)
{
    VARIANT varDN;
    VariantInit(&varDN);
    IDispatch *pDisp = NULL;
    IADs *pObject = NULL;
    //Want ou as the default category
    hr = m_pSchema->GetObject(L"classSchema",L"cn=Organizational-Unit",&pDisp);
    if (SUCCEEDED(hr))
    {
        hr = pDisp->QueryInterface(IID_IADs,(void **)&pObject);
        if (SUCCEEDED(hr))
        {
            hr = pObject->Get(L"distinguishedName",&varDN);
            if (SUCCEEDED(hr))
            {
                wcscpy(szDefaultCategory, varDN.bstrVal);
            }
        }
        if (pObject)
            pObject->Release();
    }
    if (pDisp)
        pDisp->Release();
}
    auxiliaryClass
    A multi-valued property that specifies the auxiliary classes that this class inherits from. For an existing classSchema object, values can be added to this property but not removed.

    Each value is the lDAPDisplayName of a class. Note that you must ensure that the class exists or will exist when the new class is written to the directory. If one of the classes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of auxiliary classes is the union of the systemauxiliaryClass and auxiliaryClass on this class as well as the systemauxiliaryClass and auxiliaryClass properties of all inherited classes.

    systemAuxiliaryClass
    A multi-valued property that specifies the auxiliary classes that this class inherits from. After creation of the class, this property cannot be changed.

    Each value is the lDAPDisplayName of a class. Note that you must ensure that the classes exist or will exist when the new class is written to the directory. If one of the classes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of auxiliary classes that this class inherits from is the union of the systemauxiliaryClass and auxiliaryClass on this class as well as the systemauxiliaryClass and auxiliaryClass properties of all inherited classes.

    defaultHidingValue
    Specifies the default setting of the showInAdvancedViewOnly property of new instances of this class.

    If this value is set to TRUE, it means that instances of the object are hidden in the Administrative snap-ins and the Windows shell. It also means that a menu item for the new object class will not appear in the New context menu of the Administrative snap-ins — even if the appropriate creation wizard properties are set on the new object class's displaySpecifier object.

    If this value is set to FALSE, it means that instances of the object are displayed in the Administrative snap-ins and the Windows shell. Set this property to FALSE if you want to be able to see instances of the class in the administrative snap-ins and the shell and enable a creation wizard and its menu item in the New menu of the administrative snap-ins.

    If the defaultHidingValue value is not set, the default is TRUE.

    mustContain
    A multi-valued property that specifies the attributes that must be present on instances of this class. These are mandatory attributes that must be present during creation and cannot be cleared after creation. After creation of the class, this attribute cannot be changed.

    Each value is the lDAPDisplayName of an attribute. Note that you must ensure that the attributes exist or will exist when the new class is written to the directory. If one of the attributes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of mandatory attributes for this class is the union of the systemMustContain and mustContain on this class as well as the systemMustContain and mustContain properties of all inherited classes.

    systemMustContain
    A multi-valued property that specifies the attributes that must be present on instances of this class. These are mandatory attributes that must be present during creation and cannot be cleared after creation. After creation of the class, this property cannot be changed.

    Each value is the lDAPDisplayName of an attribute. Note that you must ensure that the attributes exist or will exist when the new class is written to the directory. If one of the attributes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of mandatory attributes for this class is the union of the systemMustContain and mustContain on this class as well as the systemMustContain and mustContain properties of all inherited classes.

    mayContain
    A multi-valued property that specifies the attributes that may be present on instances of this class. These are optional attributes that are not mandatory and, therefore, may or may not be present on an instance of this class. For an existing classSchema object, values can be added to this property but not removed.

    Each value is the lDAPDisplayName of an attribute. Note that you must ensure that the attributes exist or will exist when the new class is written to the directory. If one of the attributes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of optional attributes for this class is the union of the systemMayContain and mayContain on this class as well as the systemMayContain and mayContain properties of all inherited classes.

    systemMayContain
    A multi-valued property that specifies the attributes that may be present on instances of this class. These are optional attributes that are not mandatory and, therefore, may or may not be present on an instance of this class. After creation of the class, this property cannot be changed.

    Each value is the lDAPDisplayName of an attribute. Note that you must ensure that the attributes exist or will exist when the new class is written to the directory. If one of the attributes does not exist, the classSchema object will fail to be added to the directory.

    Note that the full set of optional attributes for this class is the union of the systemMayContain and mayContain on this class as well as the systemMayContain and mayContain properties of all inherited classes.

    rDnAttId
    Specifies the ldapDisplayName of the attribute that will be the naming attribute for the new class — if different than the default ("cn").

    Use of a naming attribute other than "cn" is discouraged. Naming attributes should be drawn from the well-known set (OU, CN, O, L and DC) that is understood by all LDAP version 3 clients.

    defaultSecurityDescriptor
    Specifies the default security descriptor for new objects of this class. This security descriptor is used to provide default protection on the object if there is no security descriptor specified during the creation of the object, and if there are no object-specific inherited permissions from the parent.

    The defaultSecurityDescriptor is specified in a special string format defined in a security descriptor definition language (SDDL) syntax. This syntax is described in Microsoft® Software Developer's Kit documentation.

    For more information on the default security descriptor, see the Security chapter.

    adminDisplayName
    The display name of the class if the classDisplayName of the class's display specifier is not set or if the class's display specifier does not exist. By default, this value is the same as the cn value.
    description
    A text description of the class for documentation purposes.
  6. Write the new classSchema object to the directory.

To add an attribute to the mayContain property

  1. Retrieve the classSchema object for the class you want to modify
  2. Add the new attribute to the mayContain multi-valued property

    Note that once an attribute is added to the mayContain property, it cannot be removed.

  3. Write the changed classSchema object back to the directory.


Example code for Creating a Class

The following functions create a classSchema object in the ADSI cache.

The CreateClass function creates the classSchema object. Note that the IADs::SetInfo must be called in order to commit the new classSchema object to the directory.

The BytesToVariantArray function is a utility function that packs an octet string into a variant array.

C/C++

//Create a class with IADsContainer::Create. There is caching.
//This function creates the classSchema object but does not commit it.
//It is up to the caller to call IADs::SetInfo on the new class object
//and commit it to the ds.
HRESULT CreateClass(
    IADsContainer *pSchema,
    LPOLESTR szClassName,
    LPOLESTR szLDAPDisplayName,//
    LPOLESTR szClassOID,
    const GUID * pSchemaIDGUID,//
    LPOLESTR szSubClassOf,
    LPOLESTR *arrayAuxiliaryClasses,
    DWORD dwSizearrayAuxiliaryClasses,
    LPOLESTR szDefaultObjectCategory,
    LPOLESTR szDescription,
    BOOL bDefaultHidingValue,
    LPOLESTR szDefaultSecurityDescriptor,
    LPOLESTR szRDNAttribute,//
    int iObjectClassCategory,//
    LPOLESTR *arrayPossibleSuperiors,
    DWORD dwSizearrayPossibleSuperiors,
    LPOLESTR *arrayMustContain,
    DWORD dwSizearrayMustContain,
    LPOLESTR *arrayMayContain,
        DWORD dwSizearrayMayContain,
    LPOLESTR szAdminDisplayName,//
    IADs **ppNewClass //Return an IADs pointer to the new class.
    )
{
if ((!szClassName)
    ||(!szClassOID)
    ||(!szSubClassOf)
    ||(!iObjectClassCategory)
    ||(!arrayPossibleSuperiors)
   )
  return E_INVALIDARG;
if (!ppNewClass)
  return E_POINTER;
HRESULT hr = E_FAIL;
HRESULT hrPut = S_OK;
IDispatch *pDisp = NULL;
LPOLESTR szBuffer = new OLECHAR[MAX_PATH*2];
LPOLESTR szAttributeToSet = NULL;
wcscpy(szBuffer,L"cn=");
wcscat(szBuffer,szClassName);
(*ppNewClass) = NULL;
VARIANT var;
if (!pSchema)
  return E_POINTER;
hr = pSchema->Create(L"classSchema",szBuffer,&pDisp);
if (SUCCEEDED(hr))
{
  hr = pDisp->QueryInterface(IID_IADs, (void**)ppNewClass);
  while (SUCCEEDED(hr))
  {
    //Put cn Put this value so that it can be read from the cache.
    szAttributeToSet = L"cn";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szClassName);
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put lDAPDisplayName.
    //If NULL, let it default (ie, don't set it).
    if (szLDAPDisplayName)
    {
      szAttributeToSet = L"lDAPDisplayName";
      var.vt = VT_BSTR;
      var.bstrVal = SysAllocString(szLDAPDisplayName);
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    //Put attributeID
    szAttributeToSet = L"governsID";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szClassOID);
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put schemaIDGUID.
    //If NULL, let it default (ie, don't set it).
    if (pSchemaIDGUID)
    {
      hrPut = BytesToVariantArray(
        (LPBYTE)pSchemaIDGUID, //Pointer to bytes to put in a variant array.
        sizeof(GUID),//Size of pValue in bytes.
        &var //Return variant containing octet string (VT_UI1|VT_ARRAY).
        );
      if (SUCCEEDED(hrPut))
      {
       szAttributeToSet = L"schemaIDGUID";
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      }
      hr = hrPut;
    }

    //Put subClassOf
    szAttributeToSet = L"subClassOf";
    var.vt = VT_BSTR;
    //TODO: Check for existence of the class.
    var.bstrVal = SysAllocString(szSubClassOf);
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put defaultObjectCategory
    //If NULL, let it default (ie, don't set it).
    if (szDefaultObjectCategory)
    {
      szAttributeToSet = L"defaultObjectCategory";
      var.vt = VT_BSTR;
        //TODO: Check to ensure it is a valid DN.
      var.bstrVal = SysAllocString(szDefaultObjectCategory);
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    //Put objectClassCategory
    szAttributeToSet = L"objectClassCategory";
    var.vt = VT_I4;
    var.lVal = iObjectClassCategory;
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put defaultHidingValue
    szAttributeToSet = L"defaultHidingValue";
    var.vt = VT_BOOL;
    if (bDefaultHidingValue)
      var.boolVal = VARIANT_TRUE;
    else
      var.boolVal = VARIANT_FALSE;
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put RDNAttrID
    //If NULL, let it default (ie, don't set it).
    if (szRDNAttribute)
    {
      szAttributeToSet = L"rDNAttID";
      var.vt = VT_BSTR;
        //TODO: Check to ensure it is a valid attribute.
      var.bstrVal = SysAllocString(szRDNAttribute);
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    //Put defaultSecurityDescriptor
    //If NULL, let it default (ie, don't set it).
    if (szDefaultSecurityDescriptor)
    {
      szAttributeToSet = L"defaultSecurityDescriptor";
      var.vt = VT_BSTR;
      var.bstrVal = SysAllocString(szDefaultSecurityDescriptor);
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;
    }

    if (arrayPossibleSuperiors && dwSizearrayPossibleSuperiors)
    {
      // Build a Variant of array type, using the specified string array.
      hr = ADsBuildVarArrayStr(arrayPossibleSuperiors, dwSizearrayPossibleSuperiors, &var);
      if (SUCCEEDED(hr))
      {
        szAttributeToSet = L"possSuperiors";
            //TODO: Check to ensure that all the specified classes are valid.
        hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
        VariantClear(&var);
        hr = hrPut;
      }
    }

    if (arrayMustContain && dwSizearrayMustContain)
    {
      // Build a Variant of array type, using the specified string array.
      hr = ADsBuildVarArrayStr(arrayMustContain, dwSizearrayMustContain, &var);
      if (SUCCEEDED(hr))
      {
        szAttributeToSet = L"mustContain";
            //TODO: Check to ensure that all the specified attributes are valid.
        hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
        VariantClear(&var);
        hr = hrPut;
      }
    }

    if (arrayMayContain && dwSizearrayMayContain)
    {
      // Build a Variant of array type, using the specified string array.
      hr = ADsBuildVarArrayStr(arrayMayContain, dwSizearrayMayContain, &var);
      if (SUCCEEDED(hr))
      {
        szAttributeToSet = L"mayContain";
            //TODO: Check to ensure that all the specified attributes are valid.
        hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
        VariantClear(&var);
        hr = hrPut;
      }
    }

    if (arrayAuxiliaryClasses && dwSizearrayAuxiliaryClasses)
    {
      // Build a Variant of array type, using the specified string array.
      hr = ADsBuildVarArrayStr(arrayAuxiliaryClasses, dwSizearrayAuxiliaryClasses, &var);
      if (SUCCEEDED(hr))
      {
        szAttributeToSet = L"auxiliaryClass";
            //TODO: Check to ensure that all the specified classes are valid.
        hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
        VariantClear(&var);
        hr = hrPut;
      }
    }

    //Put description
    szAttributeToSet = L"description";
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(szDescription);
    hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
    VariantClear(&var);
    hr = hrPut;

    //Put adminDisplayName.
    //If NULL, set it to the same string as cn.

      szAttributeToSet = L"adminDisplayName";
      var.vt = VT_BSTR;
      if (!szAdminDisplayName)
      {
        var.bstrVal = SysAllocString(szClassName);
      }
      else
      {
        var.bstrVal = SysAllocString(szAdminDisplayName);
      }
      hrPut = (*ppNewClass)->Put(szAttributeToSet,var);
      VariantClear(&var);
      hr = hrPut;

    //End of properties to set.
    break;
  }
}
if (pDisp)
  pDisp->Release();

if (FAILED(hr))
{
  //Clean up if failed.
  if (*ppNewClass)
  {
    (*ppNewClass)->Release();
    (*ppNewClass) = NULL;
  }

}

return hr;
}

HRESULT BytesToVariantArray(
    PBYTE pValue, //Pointer to bytes to put in a variant array.
  ULONG cValueElements,//Size of pValue in bytes.
    VARIANT *pVariant //Return variant containing octet string (VT_UI1|VT_ARRAY).
    )
{
    HRESULT hr = E_FAIL;
    SAFEARRAY *pArrayVal = NULL;
    SAFEARRAYBOUND arrayBound;
    CHAR HUGEP *pArray = NULL;

  //Set bound for array
    arrayBound.lLbound = 0;
    arrayBound.cElements = cValueElements;

    //Create the safe array for the octet string. unsigned char elements;single dimension;aBound size.
  pArrayVal = SafeArrayCreate( VT_UI1, 1, &arrayBound );

    if (!(pArrayVal == NULL) )
    {
        hr = SafeArrayAccessData(pArrayVal, (void HUGEP * FAR *) &pArray );
    if (SUCCEEDED(hr))
    {
      //Copy the bytes to the safe array.
            memcpy( pArray, pValue, arrayBound.cElements );
            SafeArrayUnaccessData( pArrayVal );
      //Set type to array of unsigned char
        V_VT(pVariant) = VT_ARRAY | VT_UI1;
      //Assign the safe array to the array member.
            V_ARRAY(pVariant) = pArrayVal;
      hr = S_OK;
    }
    else
    {
      //Clean up if array can't be accessed.
        if ( pArrayVal )
               SafeArrayDestroy( pArrayVal );
    }
    }
  else
  {
      hr = E_OUTOFMEMORY;
  }

  return hr;
}


Disabling Existing Classes and Attributes

Schema additions are permanent. This is because in a highly distributed system like Windows 2000 it is impossible to guarantee that there are no instances of a given class or attribute. Removing the definition of a class or attribute would damage any existing instances of that class or attribute.

Instead, you can disable an existing class or attribute by marking it "defunct". This does not affect any existing instances of the class or attribute so marked, but it prevents new instances from being created.

Disabling schema classes and attributes is subject to the following restrictions:

  • You cannot disable a Category 1 class or attribute.
  • You cannot disable an attribute that is a member of a class that is not also disabled. This is because an attribute might be a "must have" for the (not disabled) class, and disabling the attribute will prevent new instances of the class from being created.

To disable an attribute, set the isDefunct attribute of its attributeSchema object to "True". When an attribute is disabled, new instances of the attribute can no longer be created. To re-enable the attribute set the isDefunct attribute to "False".

To disable a class, set the isDefunct attribute of its classSchema object to "True". When a class is disabled, new instances of the class can no longer be created. To re-enable the class set the isDefunct attribute to "False".


Installing Schema Extensions

Schema updates will be strictly controlled at most customer sites - this is the recommended practice. If a service requires schema extensions the schema extensions must be separately installable. This allows customers to perform the schema update separately and in advance of the rest of the installation.


Documenting Schema Extensions

In addition to providing a separate installation procedure for schema extensions, the nature of the schema extensions must be clearly documented. The documentation should contain:

  • A list of any new classes and attributes with an explanation of each one
  • A list of any standard classes that are extended with new attributes and a list of the attributes
  • A list of any new property pages with an explanation of how to use each one
  • An explanation of how the service uses the schema extensions


What the Installation Must Do

Programs and scripts that extend the schema should apply updates in this order:

  1. Add new attributes
  2. Add new classes
  3. Add attributes to classes. Any new attribute referenced in this step must be referred to by its OID because their names will not be present in the schema cache at this point.
  4. Trigger a cache reload. This is not necessary if the extensions will not be used immediately; the extensions will appear in the schema cache in approximately 5 minutes (dependent on system load). See section Updating the Schema Cache for information on the schema cache and how to trigger a cache reload.

Updating the Schema Cache

All information that is written to Active Directory is validated against the schema. The schema is held in memory on directory servers (domain controllers) for performance reasons. The in-memory version is updated automatically after the on-disk version has been updated. The automatic update occurs five minutes after the last change was applied; applying another change to the schema in the 5-minute window resets the timer for another 5 minutes. This behavior keeps the cache consistent, but can be confusing, since changes do not "appear" in the schema until the cache is updated, even though they were applied on disk.

To update the Active Directory schema cache after a schema update (or if you want to use the schema update for non-schema operations immediately), add the schemaUpdateNow property (it is an operational attribute) to the root DSE (blank DN) with value 1. A schema cache update will start immediately. The call is blocking. If the call returns with no error, the cache is updated and all schema updates are ready to be used. An error return indicates the cache update is not successful. Applications that want to take advantage of this feature should be designed to accommodate the blocking write, particularly in giving the user feedback, if the program or script executes interactively.

Here is a sample LDIFDE script illustrating how to trigger a cache reload:

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-


Prerequisites for Installing a Schema Extension

To modify the schema, the following things are required:

  • You must have sufficient rights to modify the schema
  • You can only make schema changes at the schema master
  • The schema master must be writeable (the safety interlock in the registry must be removed).

Schema Changes Require Sufficient Permissions

The schema contains all of the data definitions for Active Directory for the entire enterprise. To update the schema a user must be a member of the Schema Administrators group, or have been delegated the rights to update the schema.

Schema Changes Can Only Be Made at the Schema Master

Active Directory is a multi-master update system; every domain controller holds a writeable copy of the directory. Schema updates are propagated to the entire enterprise (that is, all domains that belong to the same tree or forest). Because it is difficult to reconcile conflicting updates to the schema, schema updates can only be performed at a single server. The server with the right to perform updates can change, but only one server will have that right at any given time. This server is called the Schema Master. The first DC installed in an enterprise is the Schema Master by default.

Schema changes can only be made at the schema master. You can detect which DC is the schema master by following these steps:

  1. Read the fsmoRoleOwner property of the schema container on any DC. The fsmoRoleOwner attribute returns the DN of the nTDSDSA object for the schema master.
  2. Bind to the nTDSDSA object whose DN you just retrieved. The parent of this object is the server object for the DC containing the schema master.
  3. Get the ADsPath for the parent of the nTDSDSA object. The parent is a server object.
  4. Bind to the server object.
  5. Get the dNSHostName property of the server object. This is the DNS name of the DC containing the schema master.
  6. To bind to the schema master, specify the DNS name of the schema master as the server and the DN as the DN of the schema container. For example, if the server was foobar.arcadiabay.com and the schema container DN was cn=schema,cn=configuration,dc=arcadiabay,dc=com, the ADsPath would be the following:
    LDAP://foobar.arcadiabay.com/cn=schema,cn=configuration,dc=arcadiabay,dc=com

It is highly recommended that you find the schema master and bind to it in order to make schema changes. However, you can move the schema master to another server.

To make another server the Schema Master, a suitably privileged user can:

  • Use the Schema Manager MMC-snap-in
  • Use the NTDSUTIL command-line utility
  • Use a third party program (a program that issues the appropriate LDAP write).

To become the schema master programmatically, a program running in the context of a suitably privileged user issues an LDAP write of the operational attribute becomeSchemaMaster to the rootDSE on that DC. This initiates an atomic transfer of the schema master right from the current holder to the local DC.

Detecting Which Server is the Schema Master

The following examples show how to find the schema master.

C/C++

The following function binds to the schema container on the computer that is the schema master:

HRESULT BindToSchemaMaster(IADsContainer **ppSchemaMaster)
{
HRESULT hr = E_FAIL;
//Get rootDSE and the schema container's DN.
IADs *pObject = NULL;
IADs *pTempSchema = NULL;
IADs *pNTDS = NULL;
IADs *pServer = NULL;
BSTR bstrParent;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var, varRole,varComputer;
hr = ADsOpenObject(L"LDAP://rootDSE",
         NULL,
         NULL,
         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
         IID_IADs,
         (void**)&pObject);
if (SUCCEEDED(hr))
{
  hr = pObject->Get(L"schemaNamingContext",&var);
  if (SUCCEEDED(hr))
  {
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    hr = ADsOpenObject(szPath,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IADs,
             (void**)&pTempSchema);

    if (SUCCEEDED(hr))
    {
      //Read the fsmoRoleOwner attribute to see which server is the schema master.
      hr = pTempSchema->Get(L"fsmoRoleOwner",&varRole);
      if (SUCCEEDED(hr))
      {
        //fsmoRoleOwner attribute returns the nTDSDSA object.
        //The parent is the server object.
        //Bind to NTDSDSA object and get parent
        wcscpy(szPath,L"LDAP://");
        wcscat(szPath,varRole.bstrVal);
        hr = ADsOpenObject(szPath,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IADs,
             (void**)&pNTDS);
        if (SUCCEEDED(hr))
        {
          hr = pNTDS->get_Parent(&bstrParent);
          if (SUCCEEDED(hr))
          {
            //Bind to server object
            //and get the DNS name of the server.
            wcscpy(szPath,bstrParent);
            hr = ADsOpenObject(szPath,
               NULL,
               NULL,
               ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
               IID_IADs,
               (void**)&pServer);
            if (SUCCEEDED(hr))
            {
              //Get the dns name of the server.
              hr = pServer->Get(L"dNSHostName",&varComputer);
              if (SUCCEEDED(hr))
              {
                    wcscpy(szPath,L"LDAP://");
                    wcscat(szPath,varComputer.bstrVal);
                    wcscat(szPath,L"/");
                    wcscat(szPath,var.bstrVal);
                    hr = ADsOpenObject(szPath,
                         NULL,
                         NULL,
                         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                         IID_IADs,
                         (void**)ppSchemaMaster);
                    if (FAILED(hr))
                    {
                      if (*ppSchemaMaster)
                      {
                        (*ppSchemaMaster)->Release();
                        (*ppSchemaMaster) = NULL;
                      }
                    }
              }
              VariantClear(&varComputer);
            }
            if (pServer)
              pServer->Release();
          }
          SysFreeString(bstrParent);
        }
        if (pNTDS)
          pNTDS->Release();
      }
      VariantClear(&varRole);
    }
    if (pTempSchema)
      pTempSchema->Release();
  }
  VariantClear(&var);
}
if (pObject)
    pObject->Release();

return hr;
}

VBScript

The following script displays the DNS name for the computer that is the schema master:

On Error Resume Next

''''''''''''''''''''''''''''''''''''''
'Bind to the rootDSE
'''''''''''''''''''''''''''''''''''''''
sPrefix = "LDAP://"
Set root= GetObject(sPrefix & "rootDSE")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

''''''''''''''''''''''''''''''''''''''
'Get the DN for the Schema
'''''''''''''''''''''''''''''''''''''''
sSchema = root.Get("schemaNamingContext")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get method"
End If

''''''''''''''''''''''''''''''''''''''
'Bind to the Schema container
'''''''''''''''''''''''''''''''''''''''
Set Schema= GetObject(sPrefix & sSchema )
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method to bind to Schema"
End If
'''''''''''''''''''''''''''''''''''''''
'Read the fsmoRoleOwner attribute to see which server is the schema master.
'''''''''''''''''''''''''''''''''''''''
sMaster = Schema.Get("fsmoRoleOwner")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::Get method for fsmoRoleOwner"
End If
'''''''''''''''''''''''''''''''''''''''
'fsmoRoleOwner attribute returns the nTDSDSA object.
'The parent is the server object.
'Bind to NTDSDSA object and get parent
'''''''''''''''''''''''''''''''''''''''
Set NTDS = GetObject(sPrefix & sMaster)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for NTDS"
End If
sServer = NTDS.Parent
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::get_Parent method"
End If
'''''''''''''''''''''''''''''''''''''''
'Bind to server object
'and get the reference to the computer object.
'''''''''''''''''''''''''''''''''''''''
Set Server = GetObject(sServer)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for " & sServer
End If
sComputer = Server.Get("dNSHostName")
'''''''''''''''''''''''''''''''''''''''
'Display the DNS name for the computer.
'''''''''''''''''''''''''''''''''''''''
strText = "Schema Master has the following DNS name: "& sComputer
WScript.echo strText

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Enabling Schema Changes at the Schema Master

Schema modification is disabled by default on all Windows NT 5.0 domain controllers. To enable schema modification at a particular DC, you must insert a value (or ensure that it is greater than zero if it already exists) into the registry on that DC. You must add the value "Schema Update Allowed" as a REG_DWORD value and set the value to 1 (or any value greater than 0). This value must be added to the following registry key:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS\Parameters

Note that If the "Schema Update Allowed" registry value is set to zero (0), then schema modification is turned off.

The Schema Manager MMC snap-in provides a checkbox that sets or clears this registry key.

Example

The following function does checks and if necessary sets the Schema Update Allowed value:

  1. Connects to the registry of the specified computer.
  2. Tries to read the Schema Update Allowed value
  3. If that value does not exist or if the value is not greater than 0, asks the user if the value should be set. If Yes, attempts to create the value (if necessary) and set it to 1.
  4. Returns S_OK if value is set or S_FALSE if it is not.

C/C++

HRESULT IsSchemaMasterWritable(LPOLESTR szSchemaMasterComputerName)
{
  if (!szSchemaMasterComputerName)
    return E_POINTER;
  HRESULT hr = E_FAIL;
  LONG lReturn;
  HKEY hKey,hKeyTarget;
  LPOLESTR szRelKeyPath = L"System\\CurrentControlSet\\Services\\NTDS\\Parameters";
  LPOLESTR szValueName = L"Schema Update Allowed";
  LPOLESTR szPath = new OLECHAR[MAX_PATH];
  LPOLESTR szBuffer = new OLECHAR[MAX_PATH];
  ULONG lSize = sizeof(DWORD);
  DWORD dwType,dwValue;
  wcscpy(szPath,L"\\\\");
  wcscat(szPath,szSchemaMasterComputerName);
  lReturn = RegConnectRegistry(szPath,HKEY_LOCAL_MACHINE,&hKey);
  if (ERROR_SUCCESS==lReturn)
  {
    lReturn = RegOpenKeyEx(hKey,szRelKeyPath,0,KEY_READ,&hKeyTarget);
    if (ERROR_SUCCESS==lReturn)
    {
      lReturn = RegQueryValueEx(hKeyTarget,szValueName,0,&dwType,(LPBYTE)&dwValue,&lSize);
      if (ERROR_SUCCESS==lReturn)
      {
        if (dwValue>0)
          hr = S_OK;
        else
          hr = S_FALSE;
      }
      //if the value could not be found, return S_FALSE.
      else if (ERROR_FILE_NOT_FOUND==lReturn)
      {
        hr = S_FALSE;
      }

      if (S_FALSE==hr)
      {
        RegCloseKey(hKeyTarget);
        int iSetIfNotWritable = MessageBox(NULL,L"The schema master is not writeable. Do you want to make it writeable?",L"Extend Schema Wizard",MB_YESNO|MB_ICONEXCLAMATION);
        if (iSetIfNotWritable==IDYES)
        {
          lReturn = RegOpenKeyEx(hKey,szRelKeyPath,0,KEY_WRITE,&hKeyTarget);
          if (ERROR_SUCCESS==lReturn)
          {
            //Set the value to 1.
            dwValue = 1L;
            lReturn = RegSetValueEx(hKeyTarget,szValueName,0L,REG_DWORD,(LPBYTE)&dwValue,lSize);
            if (ERROR_SUCCESS==lReturn)
            {
              hr = S_OK;
            }
            else
            {
               MessageBox(NULL,L"Could not set the Schema Master as writeable.",L"Extend Schema Wizard",MB_OK|MB_ICONEXCLAMATION);
            }
          }
        }
      }
    }
    RegCloseKey(hKeyTarget);
  }
  RegCloseKey(hKey);
  return hr;
}


Recommendations for Schema Extension Programs

In addition to the prerequisites, the following practices are recommended for schema extension programs:

  1. Find the Schema Master. Bind to the schema on the DC that is the schema master. Avoid unnecessarily changing the schema master role between DCs. To bind to the schema container on the schema master, see the sample code in Prerequisites for Installing a Schema Extension.
  2. Before doing anything, check the allowedChildClassesEffective property of the Schema container to see if you can create attributes and/or classes. If attributeSchema and classSchema are not values in that property, you do not have sufficient rights to add attributes or classes to the schema. See the sample code below.
  3. Make sure that the Schema Update Allowed is set appropriately in the registry of the schema master. If you need to create or set this value, restore it to its original state as part of your program's clean-up routine. To check and set this value, see the sample code in Prerequisites for Installing a Schema Extension.
  4. Before adding attributes or classes, make sure that they do not already exist. If they do exist, verify that they are the same attributes or classes you are adding and not an attribute or class created by someone else with different syntax and properties that are incompatible with your attributes or classes.

    For attributes, query for cn, attributeID, governsID, lDAPDisplayName, and schemaIDGUID to ensure they are not already used. If you are adding a set of linked attributes (one forward link, one back link), make sure that the linkIDs are not already used. Note that you must query for governsID as well because the OID must be unique among attributes and classes.

    For classes, query for cn, governsId, attributeID, lDAPDisplayName, and schemaIDGUID to ensure they are not already used. Note that you must query for attributeID as well because the OID must be unique among classes and attributes.

    To check for naming collisions, see the sample code below.

    If attributes or classes exist that conflict with your new attributes or classes, your program should not apply your schema changes.

  5. If such a collision exists, your program should not apply your schema changes. The schema administrator may need to resolve the collision and then run your program again. Alternatively, a different lDAPDisplayName could be used; however, any applications using the attribute or object must be aware of that change. To help avoid OID collisions, obtain an OID from an ISO name registration authority. To avoid naming collisions, it is recommended that you register your schema extension prefixes with Microsoft. To request or register a prefix from Microsoft, send e-mail to the following address:

    oids@microsoft.com

  6. If your program is dependent on attributes or classes that it has added, update the schema cache before adding the new attributes or classes that are dependent on those attributes or classes. Note that the schemaUpdateNow operational attribute is synchronous. This means your Put method call will block until the schema cache is updated. When the call returns, the schema cache has been updated and the new attributes and/or classes are accessible.

    To update the schema cache, see the sample code below.


Example Code for Checking for Rights to Create attributeSchema and classSchema objects

The following function checks the allowedChildClassesEffective property on the schema container (IADs pointer to schema container is passed as a parameter) for the attributeSchema and classSchema classes. It returns S_OK if both classes are listed in allowedChildClassesEffective. If both are not, it returns S_FALSE.

C/C++

//Function takes an IADs pointer to schema container as a parameter.

HRESULT HasSchemaAdditionRights(IADs *pSchema)
{
if (!pSchema)
  return E_POINTER;
HRESULT hr = E_FAIL;
VARIANT var;
VARIANT *pVar;
//Check access rights
BOOL bIsAddAttributeAllowed = FALSE;
BOOL bIsAddClassAllowed = FALSE;
//Get AllowedChildClassesEffective. It is constructed;
//therefore, you must use GetInfoEx to explicitly get it into the cache.
LPOLESTR pwszArray[] = {L"allowedChildClassesEffective"};
DWORD dwArrayItems = sizeof(pwszArray)/sizeof(LPOLESTR);
VARIANT vArray;
VariantInit(&vArray);
// Build a Variant of array type, using the specified string array.
hr = ADsBuildVarArrayStr(pwszArray, dwArrayItems, &vArray);
if (SUCCEEDED(hr))
{
  hr = pSchema->GetInfoEx(vArray,0L);
  if (SUCCEEDED(hr))
  {
    hr = pSchema->GetEx(L"allowedChildClassesEffective", &var);
    if (SUCCEEDED(hr))
    {
       hr = SafeArrayAccessData((SAFEARRAY*)(var.pparray), (void HUGEP* FAR*)&pVar);
       long lLBound, lUBound;
       //One dimensional array. Get the bounds for the array.
        hr = SafeArrayGetLBound((SAFEARRAY*)(var.pparray), 1, &lLBound);
       hr = SafeArrayGetUBound((SAFEARRAY*)(var.pparray), 1, &lUBound);
       //Get the count of elements
       long cElements = lUBound-lLBound + 1;
       //Get the elements of the array
       if (SUCCEEDED(hr))
       {
        for (int i = 0; i < cElements; i++ )
        {
          //Check each element to see if attributeSchema or classSchema is there.
          if (VT_BSTR == pVar[i].vt)
          {
            if (0==_wcsicmp(L"attributeSchema", pVar[i].bstrVal))
              bIsAddAttributeAllowed = TRUE;
            else if (0==_wcsicmp(L"classSchema", pVar[i].bstrVal))
              bIsAddClassAllowed = TRUE;
          }
        }
        if (bIsAddAttributeAllowed && bIsAddClassAllowed)
          hr = S_OK;
        else
          hr = S_FALSE;
       }
    }
    VariantClear(&var);
  }
}

return hr;

};


Example Code for Updating the Schema Cache

The following functions get the DNS name of the schema master (GetSchemaMasterDNSName) and update the schema cache at the schema master (UpdateSchemaMasterCache). To use these functions, call GetSchemaMasterDNSName to get the schema master DNS name, pass the schema master DNS name to UpdateSchemaMasterCache, call CoTaskMemFree on string containing the schema master DNS name.

C/C++

HRESULT UpdateSchemaMasterCache(LPOLESTR szSchemaMasterDNSName)
{
if (!szSchemaMasterDNSName)
   return E_POINTER;
HRESULT hr = E_FAIL;
IADs *pObject = NULL;
VARIANT var;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
wcscpy(szPath,L"LDAP://");
wcscat(szPath,szSchemaMasterDNSName);
wcscat(szPath,L"/rootDSE");
hr = ADsOpenObject(szPath,
         NULL,
         NULL,
         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
         IID_IADs,
         (void**)&pObject);
if (SUCCEEDED(hr))
{
  VariantInit(&var);
  var.vt = VT_I4;
  var.lVal = 1L;
  hr = pObject->Put(L"schemaUpdateNow",var);
  if (SUCCEEDED(hr))
  {
    hr = pObject->SetInfo();
    if (SUCCEEDED(hr))
      MessageBox(NULL,L"Updated",L"Extend Schema Wizard",MB_OK|MB_ICONEXCLAMATION);
    else
      MessageBox(NULL,L"Update Failed",L"Extend Schema Wizard",MB_OK|MB_ICONEXCLAMATION);
  }
}
if (pObject)
  pObject->Release();

return hr;
}

HRESULT GetSchemaMasterDNSName(LPOLESTR *pszSchemaMasterDNSName)
{
if (!pszSchemaMasterDNSName)
   return E_POINTER;
HRESULT hr = E_FAIL;
IADs *pObject = NULL;
IADs *pTempSchema = NULL;
IADs *pNTDS = NULL;
IADs *pServer = NULL;
BSTR bstrParent;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var, varRole,varComputer;
hr = ADsOpenObject(L"LDAP://rootDSE",
         NULL,
         NULL,
         ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
         IID_IADs,
         (void**)&pObject);
if (SUCCEEDED(hr))
{
  hr = pObject->Get(L"schemaNamingContext",&var);
  if (SUCCEEDED(hr))
  {
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    hr = ADsOpenObject(szPath,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IADs,
             (void**)&pTempSchema);

    if (SUCCEEDED(hr))
    {
      //Read the fsmoRoleOwner attribute to see which server is the schema master.
      hr = pTempSchema->Get(L"fsmoRoleOwner",&varRole);
      if (SUCCEEDED(hr))
      {
        //fsmoRoleOwner attribute returns the nTDSDSA object.
        //The parent is the server object.
        //Bind to NTDSDSA object and get parent
        wcscpy(szPath,L"LDAP://");
        wcscat(szPath,varRole.bstrVal);
        hr = ADsOpenObject(szPath,
             NULL,
             NULL,
             ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
             IID_IADs,
             (void**)&pNTDS);
        if (SUCCEEDED(hr))
        {
          hr = pNTDS->get_Parent(&bstrParent);
          if (SUCCEEDED(hr))
          {
            //Bind to server object
            //and get the reference to the computer object.
            wcscpy(szPath,bstrParent);
            hr = ADsOpenObject(szPath,
               NULL,
               NULL,
               ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
               IID_IADs,
               (void**)&pServer);
            if (SUCCEEDED(hr))
            {
              //Get the dns name of the server.
              hr = pServer->Get(L"dNSHostName",&varComputer);
              if (SUCCEEDED(hr))
              {
                *pszSchemaMasterDNSName = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(varComputer.bstrVal)+1));
                if (*pszSchemaMasterDNSName)
                {
                  wcscpy(*pszSchemaMasterDNSName, varComputer.bstrVal);
                }
                else
                {
                  hr = E_OUTOFMEMORY;
                }
              }
              VariantClear(&varComputer);
            }
            if (pServer)
              pServer->Release();
          }
          SysFreeString(bstrParent);
        }
        if (pNTDS)
          pNTDS->Release();
      }
      VariantClear(&varRole);
    }
    if (pTempSchema)
      pTempSchema->Release();
  }
  VariantClear(&var);
}
if (pObject)
    pObject->Release();

return hr;
}


Example Code for Detecting Schema Naming Collisions

The following function queries the schema for the key naming attributes on a classSchema or attributeSchema object.

It returns TRUE if conflicting attributes or classes are found.

It returns FALSE if the attribute or class with the specified cn, lDAPDisplayName, OID, schemaIDGUID, or linkID does not conflict with the schema and, therefore, is safe to add to the schema.

C/C++

HRESULT FindCollidingAttributesOrClasses(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
    LPOLESTR szName,
    LPOLESTR szLDAPDisplayName,
    LPOLESTR szOID,
    const GUID * pSchemaIDGUID,
    DWORD dwLinkID, //Set 0L if attribute is not linked or if checking for class.
    BOOL *pbIsColliding//Return result.
              )
{
if (
    (!pSchemaNC)
  ||(!szName)
  ||(!szLDAPDisplayName)
  ||(!szOID)
  ||(!pSchemaIDGUID)
   )
  return E_POINTER;

LPOLESTR szFilter = new OLECHAR(MAX_PATH*5);
LPOLESTR szBuffer = NULL;

HRESULT hr = E_FAIL;

hr = ADsEncodeBinaryData((LPBYTE)pSchemaIDGUID,sizeof(GUID),&szBuffer);
if (SUCCEEDED(hr))
{
    if (dwLinkID>0L)
      swprintf(szFilter,L"(|(cn=%s)(lDAPDisplayName=%s)(attributeID=%s)(governsID=%s)(schemaIDGUID=%s)(linkID=%d))",szName,szLDAPDisplayName,szOID,szOID,szBuffer,dwLinkID);
    else
      swprintf(szFilter,L"(|(cn=%s)(lDAPDisplayName=%s)(attributeID=%s)(governsID=%s)(schemaIDGUID=%s))",szName,szLDAPDisplayName,szOID,szOID,szBuffer);
    //Attributes are one-level deep in the Schema container so only need to search one level.
  ADS_SEARCHPREF_INFO SearchPrefs;
  SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
  SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
  SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
    DWORD dwNumPrefs = 1;

  // COL for iterations
  LPOLESTR pszColumn = NULL;
  ADS_SEARCH_COLUMN col;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch = NULL;

    int iCount = 0;

  // Set the search preference
    hr = pSchemaNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);
    if (SUCCEEDED(hr))
  {
    LPOLESTR pszNonVerboseList[] = {L"lDAPDisplayName",L"cn",L"attributeID",L"governsID",L"schemaIDGUID",L"linkID",L"objectCategory"};

    DWORD x = 0L;

    hr = pSchemaNC->ExecuteSearch(szFilter,
                    pszNonVerboseList,
                    sizeof(pszNonVerboseList)/sizeof(LPOLESTR),
                    &hSearch
                    );
     if ( SUCCEEDED(hr) )
    {
    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
      hr = pSchemaNC->GetFirstRow( hSearch);
      if (SUCCEEDED(hr))
      {
      while( hr != S_ADS_NOMORE_ROWS )
      {
        //Keep track of count.
        iCount++;
          //Get the next row
         hr = pSchemaNC->GetNextRow( hSearch);
      }

      }
      // Close the search handle to clean up
      pSchemaNC->CloseSearchHandle(hSearch);
    }
  }
  if (SUCCEEDED(hr))
  {
    if (iCount==0)
    {
      *pbIsColliding = FALSE;
      hr = S_FALSE;
    }
    else if (iCount>0)
    {
      *pbIsColliding = TRUE;
      hr = S_OK;
    }
    else
      hr = E_FAIL;
  }
}
if(szBuffer)
  FreeADsMem(szBuffer);
    return hr;
}


Supported Installation Mechanisms

When extending the Active Directory schema, you have four choices:

  1. Extend using LDIF scripts. This is the preferred mechanism.
  2. Extend using Comma-separated Value (CSV) scripts.
  3. Extend programmatically.
  4. Extend using the user interface.


LDIF Scripts

LDIF stands for LDAP Data Interchange Format and is a standard as defined by the IETF LDAPEXT working group (http://www.ietf.org/html.charters/ldapext-charter.html) in the Internet Draft draft-good-ldap-ldif-02.txt. Windows 2000 includes a command-line utility, LDIFDE, to import directory objects using LDIF files and export directory objects as LDIF files. Although LDIFDE scripts don't support Unicode, schema administrators easily read the scripts. For Unicode or binary values, you must base64 encode the values. Base64 encoding is defined in RFC 2045, Section 6.8.

Note that an LDIF file must apply schema changes so that dependencies are accounted for (such as forward link attributes should be added before the corresponding back link attribute) and schema cache updates should be forced before adding classes that depend on attributes or classes added earlier in the LDIF script. See the example below.

Example

This example consists of the following:

  1. MYSCHEMAEXT.LDF is an LDIF script containing new attributes and classes. Note that this file is a modified version of the file generated from LGETATTCLS.VBS. Note that the My-Test-Attribute-DN-FL attribute was moved ahead of My-Test-Attribute-DN-BL because the back link (My-Test-Attribute-DN-BL) is dependent on the forward link (My-Test-Attribute-DN-FL). Also note that the schemaUpdateNow operational attribute is set in two places to trigger updates of the schema cache so that dependent attributes and classes will be available for adding the two classes in the script.
  2. LGETATTCLS.VBS is a VBScript file that generates the LDIF script used as the starting point for the MYSCHEMAEXT.LDF file. Note that the current schema path is replaced by CN=Schema,CN=Configuration,DC=myorg,DC=com. You can replace DC=myorg,DC=com to reflect whatever DN you want to publish in the LDIF script—just make sure that LSETATTCLS.VBS reflects the change in its sFromDN so that the correct DN is replaced when the LDIF script is applied. Also note that the script uses a prefix to find the classes and attributes—you should also define and use a prefix for all your classes and attributes (see the section on naming attributes and classes). In addition, the script outputs only the necessary attributes for the attributeSchema and classSchema objects to the LDIF file.
  3. LGETATTCLS.VBS is a VBScript file that uses the MYSCHEMAEXT.LDF script to add the new attributes and classes in the script. You must ensure that the schema master is writable before running the script.

MYSCHEMAEXT.LDF

dn: CN=My-Test-Attribute-CaseExactString,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-CaseExactString
attributeID: 1.2.840.113556.1.4.7000.159.24.10.65
attributeSyntax: 2.5.5.3
cn: My-Test-Attribute-CaseExactString
description:
 Test attribute of syntax CaseExactString used to show how to add a CaseExactSt
 ring attribute.
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeCaseExactString
distinguishedName:
 CN=My-Test-Attribute-CaseExactString,CN=Schema,CN=Configuration,DC=myorg,DC=co
 m
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMSyntax: 27
name: My-Test-Attribute-CaseExactString
schemaIDGUID:: 6ASznA3W0hGBpwDAT7mMGg==
searchFlags: 0

dn: CN=My-Test-Attribute-DN-FL,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-DN-FL
attributeID: 1.2.840.113556.1.4.7000.159.24.10.614
attributeSyntax: 2.5.5.1
cn: My-Test-Attribute-DN-FL
description:
 Test forward link attribute of syntax DN used to show how to add a forward link
 k attribute. Back link is My-Test-Attribute-DN-BL.
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeDNFL
linkID: 146
distinguishedName:
 CN=My-Test-Attribute-DN-FL,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMObjectClass:: KwwCh3McAIVK
oMSyntax: 127
rangeLower: 0
rangeUpper: 257
name: My-Test-Attribute-DN-FL
schemaIDGUID:: YGLudffa0hGLEwDAT7mMGg==
searchFlags: 0

dn: CN=My-Test-Attribute-DN-BL,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-DN-BL
attributeID: 1.2.840.113556.1.4.7000.159.24.10.615
attributeSyntax: 2.5.5.1
cn: My-Test-Attribute-DN-BL
description:
 Test back link attribute of syntax DN used to show how to add a back link attribute. Forward link is My-Test-Attribute-DN-FL.
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeDNBL
linkID: 147
distinguishedName:
 CN=My-Test-Attribute-DN-BL,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMObjectClass:: KwwCh3McAIVK
oMSyntax: 127
rangeLower: 0
rangeUpper: 257
name: My-Test-Attribute-DN-BL
schemaIDGUID:: jFfbhffa0hGLEwDAT7mMGg==
searchFlags: 0

dn: CN=My-Test-Attribute-DN-Regular,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-DN-Regular
attributeID: 1.2.840.113556.1.4.7000.159.24.10.613
attributeSyntax: 2.5.5.12
cn: My-Test-Attribute-DN-Regular
description:
 Test attribute of syntax DN used to show how to add a DN attribute.
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeDNRegular
distinguishedName:
 CN=My-Test-Attribute-DN-Regular,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMObjectClass:: KwwCh3McAIVK
oMSyntax: 64
rangeLower: 0
rangeUpper: 257
name: My-Test-Attribute-DN-Regular
schemaIDGUID:: 5QSznA3W0hGBpwDAT7mMGg==
searchFlags: 0

dn: CN=My-Test-Attribute-DNString,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-DNString
attributeID: 1.2.840.113556.1.4.7000.159.24.10.611
attributeSyntax: 2.5.5.14
cn: My-Test-Attribute-DNString
description:
 Test attribute of syntax DNString used to show how to add a DNString attribute
 .
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeDNString
distinguishedName:
 CN=My-Test-Attribute-DNString,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMObjectClass:: KoZIhvcUAQEBDA==
oMSyntax: 127
rangeLower: 1
rangeUpper: 64
name: My-Test-Attribute-DNString
schemaIDGUID:: 5ASznA3W0hGBpwDAT7mMGg==
searchFlags: 0

dn: CN=My-Test-Attribute-UnicodeString,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Attribute-UnicodeString
attributeID: 1.2.840.113556.1.4.7000.159.24.10.612
attributeSyntax: 2.5.5.12
cn: My-Test-Attribute-UnicodeString
description:
 Test attribute of syntax Unicode String (DirectoryString) used to show how to
 add a Unicode String attribute.
isMemberOfPartialAttributeSet: FALSE
isSingleValued: TRUE
lDAPDisplayName: myTestAttributeUnicodeString
distinguishedName:
 CN=My-Test-Attribute-UnicodeString,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: attributeSchema
oMSyntax: 64
rangeLower: 0
rangeUpper: 257
name: My-Test-Attribute-UnicodeString
schemaIDGUID:: 9QSznA3W0hGBpwDAT7mMGg==
searchFlags: 0

DN:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

dn: CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Auxiliary-Class1
cn: My-Test-Auxiliary-Class1
defaultHidingValue: FALSE
defaultObjectCategory:
 CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=myorg,DC=com
description: Test class used to show how to add an auxiliary class.
governsID: 1.2.840.113556.1.4.7000.159.24.10.611.11
lDAPDisplayName: myTestAuxiliaryClass1
mayContain: myTestAttributeDNString
mustContain: myTestAttributeUnicodeString
mustContain: description
distinguishedName:
 CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Class-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: classSchema
objectClassCategory: 3
possSuperiors: container
possSuperiors: domainDNS
name: My-Test-Auxiliary-Class1
rDNAttID: cn
schemaIDGUID:: mmsxdsXb0hGL0AAA+HW2YA==
subClassOf: top

DN:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

dn: CN=My-Test-Structural-Class1,CN=Schema,CN=Configuration,DC=myorg,DC=com
changetype: add
adminDisplayName: My-Test-Structural-Class1
auxiliaryClass: myTestAuxiliaryClass1
cn: My-Test-Structural-Class1
defaultHidingValue: FALSE
defaultObjectCategory:
 CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=myorg,DC=com
defaultSecurityDescriptor:
 D:(A;;RPWPCRCCDCLCLOLORCWOWDSDDTDTSW;;;DA)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)
 (A;;RPLCLORC;;;AU)
description: Test class used to show how to add a structure class.
governsID: 1.2.840.113556.1.4.7000.159.24.10.611.12
lDAPDisplayName: myTestStructuralClass1
mayContain: myTestAttributeDNFL
mayContain: wWWHomePage
mustContain: url
distinguishedName:
 CN=My-Test-Structural-Class1,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectCategory: CN=Class-Schema,CN=Schema,CN=Configuration,DC=myorg,DC=com
objectClass: classSchema
objectClassCategory: 1
possSuperiors: organizationalUnit
name: My-Test-Structural-Class1
rDNAttID: ou
schemaIDGUID:: 1HsnsL7b0hGL0AAA+HW2YA==
subClassOf: organizationalUnit

LGETATTCLS.VBS

On Error Resume Next

''''''''''''''''''''''''''''''''''''''
'Bind to the rootDSE
'''''''''''''''''''''''''''''''''''''''
sPrefix = "LDAP://"
Set root= GetObject(sPrefix & "rootDSE")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

''''''''''''''''''''''''''''''''''''''
'Get the DN for the Schema
'''''''''''''''''''''''''''''''''''''''
sSchema = root.Get("schemaNamingContext")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get method"
End If

''''''''''''''''''''''''''''''''''''''
'Bind to the Schema container
'''''''''''''''''''''''''''''''''''''''
Set Schema= GetObject(sPrefix & sSchema )
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method to bind to Schema"
End If
'''''''''''''''''''''''''''''''''''''''
'Read the fsmoRoleOwner attribute to see which server is the schema master.
'''''''''''''''''''''''''''''''''''''''
sMaster = Schema.Get("fsmoRoleOwner")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::Get method for fsmoRoleOwner"
End If
'''''''''''''''''''''''''''''''''''''''
'fsmoRoleOwner attribute returns the nTDSDSA object.
'The parent is the server object.
'Bind to NTDSDSA object and get parent
'''''''''''''''''''''''''''''''''''''''
Set NTDS = GetObject(sPrefix & sMaster)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for NTDS"
End If
sServer = NTDS.Parent
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::get_Parent method"
End If
'''''''''''''''''''''''''''''''''''''''
'Bind to server object
'and get the reference to the computer object.
'''''''''''''''''''''''''''''''''''''''
Set Server = GetObject(sServer)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for " & sServer
End If
'''''''''''''''''''''''''''''''''''''''
'Display the DN for the computer object.
'''''''''''''''''''''''''''''''''''''''
sComputerDNSName = Server.Get("DNSHostName")
strText = "Schema Master has the following DNS Name: "& sComputerDNSName
WScript.echo strText

sFile = "myschemaext.ldf"
sFromDN = sSchema
sToDN = "CN=Schema,CN=Configuration,DC=myorg,DC=com"
sAttrPrefix = "My-Test"
sFilter = "(&((cn=" & sAttrPrefix & "*)(|(objectCategory=classSchema)(objectCategory=attributeSchema))))"
sRetAttr = "dn,adminDescription,adminDisplayName,governsID,cn,mayContain,mustContain,systemMayContain,systemMustContain,lDAPDisplayName,objectClassCategory,distinguishedName,objectCategory,objectClass,possSuperiors,systemPossSuperiors,subClassOf,defaultObjectCategory,name,schemaIDGUID,auxiliaryClass,auxiliaryClass,systemAuxiliaryClass,description,defaultHidingValue,rDNAttId,defaultSecurityDescriptor,attributeID,attributeSecurityGUID,attributeSyntax,isMemberOfPartialAttributeSet,isSingleValued,mAPIID,oMSyntax,rangeLower,rangeUpper,searchFlags,oMObjectClass,linkID"

'Add flag rootDN--we want schema.
sCommand = "ldifde -d " & sSchema
sCommand = sCommand & " -c " & sFromDN & " " & sToDN
'Add flag schema master
sCommand = sCommand & " -s " & sComputerDNSName
'Add flag filename
sCommand = sCommand & " -f " & sFile
'Add flag filter to search for my attributes
sCommand = sCommand & " -r " & sFilter
'Add flag for attributes to return
sCommand = sCommand & " -l " & sRetAttr

WScript.echo sCommand
Set WshShell = Wscript.CreateObject("Wscript.Shell")
WshShell.Run (sCommand)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub

LSETATTCLS.VBS

On Error Resume Next

''''''''''''''''''''''''''''''''''''''
'Bind to the rootDSE
'''''''''''''''''''''''''''''''''''''''
sPrefix = "LDAP://"
Set root= GetObject(sPrefix & "rootDSE")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

''''''''''''''''''''''''''''''''''''''
'Get the DN for the Schema
'''''''''''''''''''''''''''''''''''''''
sSchema = root.Get("schemaNamingContext")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get method"
End If

''''''''''''''''''''''''''''''''''''''
'Bind to the Schema container
'''''''''''''''''''''''''''''''''''''''
Set Schema= GetObject(sPrefix & sSchema )
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method to bind to Schema"
End If
'''''''''''''''''''''''''''''''''''''''
'Read the fsmoRoleOwner attribute to see which server is the schema master.
'''''''''''''''''''''''''''''''''''''''
sMaster = Schema.Get("fsmoRoleOwner")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::Get method for fsmoRoleOwner"
End If
'''''''''''''''''''''''''''''''''''''''
'fsmoRoleOwner attribute returns the nTDSDSA object.
'The parent is the server object.
'Bind to NTDSDSA object and get parent
'''''''''''''''''''''''''''''''''''''''
Set NTDS = GetObject(sPrefix & sMaster)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for NTDS"
End If
sServer = NTDS.Parent
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::get_Parent method"
End If
'''''''''''''''''''''''''''''''''''''''
'Bind to server object
'and get the reference to the computer object.
'''''''''''''''''''''''''''''''''''''''
Set Server = GetObject(sServer)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method for " & sServer
End If
sComputer = Server.Get("serverReference")
'''''''''''''''''''''''''''''''''''''''
'Display the DN for the computer object.
'''''''''''''''''''''''''''''''''''''''
sComputerDNSName = Server.Get("DNSHostName")
'strText = "Schema Master has the following DN: "& sComputer
strText = "Schema Master has the following DNS Name: "& sComputerDNSName
WScript.echo strText

sFile = "myschemaext.ldf"
sFromDN = "CN=Schema,CN=Configuration,DC=myorg,DC=com"
sToDN = sSchema
'Add flag replace fromDN with ToDN.
sCommand = "ldifde -i -k -c " & sFromDN & " " & sToDN
'Add flag schema master
sCommand = sCommand & " -s " & sComputerDNSName
'Add flag filename
sCommand = sCommand & " -f " & sFile
'Add flag filter to search for my attributes

WScript.echo sCommand
Set WshShell = Wscript.CreateObject("Wscript.Shell")
WshShell.Run (sCommand)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Comma-separated Value (CSV) Scripts

Windows 2000 includes a command-line utility, CSVDE, to import directory objects using .CSV files and export directory objects as .CSV files. CSV scripts are targeted for ease-of-use. The first line in the script identifies the attributes in the lines that follow. Columns are separated by commas. The file format is compatible with the Microsoft Excel CSV format, so that files are easily created. Use Excel or any other tool that can read and write .CSV files. A benefit of using CSVDE is that it supports Unicode.

Example CSV File

The following CSV file adds an auxiliary class:

DN,adminDisplayName,cn,defaultHidingValue,defaultObjectCategory,description,governsID,lDAPDisplayName,mayContain,mustContain,distinguishedName,objectCategory,objectClass,objectClassCategory,possSuperiors,name,rDNAttID,schemaIDGUID,subClassOf,auxiliaryClass,defaultSecurityDescriptor
"CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=timtobigboxdom,DC=nttest,DC=microsoft,DC=com",My-Test-Auxiliary-Class1,My-Test-Auxiliary-Class1,FALSE,"CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=timtobigboxdom,DC=nttest,DC=microsoft,DC=com",Test class used to show how to add an auxiliary class.,1.2.840.113556.1.4.7000.159.24.10.611.11,myTestAuxiliaryClass1,myTestAttributeDNString,description;myTestAttributeUnicodeString,"CN=My-Test-Auxiliary-Class1,CN=Schema,CN=Configuration,DC=timtobigboxdom,DC=nttest,DC=microsoft,DC=com","CN=Class-Schema,CN=Schema,CN=Configuration,DC=timtobigboxdom,DC=nttest,DC=microsoft,DC=com",classSchema,3,domainDNS;container,My-Test-Auxiliary-Class1,cn,X'9a6b3176c5dbd2118bd00000f875b660',top,,


Programmatic Extension

  • The benefit of an LDIF file is that the administrator can look at it to see what it does. However, the schema can also be extended programmatically. Programmatic extensions have several merits (see below), however, a program is opaque; the administrator must trust the documentation shipped with the setup program. Use of programmatic schema extensions may be a barrier to deployment for applications that use them. A programmatic extension is invariant: it's an NT executable. The binary cannot be tampered with unlike an LDIF or .CSV file, which can be edited inadvertently or maliciously.
  • Programs can detect and recover from errors and provide intelligent feedback to the user.
  • Programs handle UNICODE without resorting to Base64 encoding.
  • Programs can leverage the MSI setup APIs.
  • Programs can be signed to prove their authenticity


Example Code for Extending the Schema Programmatically

/*
**  schema.cxx: Example of installing an Active Directory Schema extension.
**
**    This example creates 6 new attributes, 1 new class, and modifies
**    two existing classes by adding new MAY HAVE attributes. These are used
**    in the DS Enabled Applications sample programs for the 1998 PDC.
**
**    Attributes:    courseBattleCruiser       integer
**                   speedBattleCruiser        integer
**                   maxPayloadBattleCruiser   integer
**                   allowedBattleCruiser      integer
**                   consistencyGUID           octet string
**                   consistencyChildCount     integer
**
**                Class:    policyParametersBattleCruiser
**
**                Classes Modified: container, groupPolicyContainer
**
**                The modified classes have the two consistency attributes added.
**
**  Notes:        Must be run on a DC which is the current Schema Master and has
**                schema updates enabled using the registry key and value:
**
**                KEY:  HKLM\CurrentControlSet\Services\NTDS\Parameters
**                Value:    Schema Update Allowed, REG_DWORD, 1
**
**  Libraries:  activeds.lib, adsiid.lib
**
**  Copyright (c) 1997, 1998 Microsoft Corporation
**  All Rights Reserved
**
*/

#include "stdafx.h"

// forward declaration of error report

void ReportError(TCHAR *defaultMsg, DWORD dwErr);

// GUIDs for schema extensions. Provide your own GUID for each extension so
// they are the same in every installation. If the DS assigns them your GUIDs
// will be different in every installation, which will hurt you down the road
// when we move to an identity-based schema.

// GUIDS for Battle Cruiser Policy attributes

// {C45F05B2-4D16-11d2-800E-0080C76670C0}
static const GUID attrGuid1 =
{ 0xc45f05b2, 0x4d16, 0x11d2, { 0x80, 0xe, 0x0, 0x80, 0xc7, 0x66, 0x70, 0xc0 } };
// {C45F05B3-4D16-11d2-800E-0080C76670C0}
static const GUID attrGuid2 =
{ 0xc45f05b3, 0x4d16, 0x11d2, { 0x80, 0xe, 0x0, 0x80, 0xc7, 0x66, 0x70, 0xc0 } };
// {C45F05B4-4D16-11d2-800E-0080C76670C0}
static const GUID attrGuid3 =
{ 0xc45f05b4, 0x4d16, 0x11d2, { 0x80, 0xe, 0x0, 0x80, 0xc7, 0x66, 0x70, 0xc0 } };
// {C45F05B6-4D16-11d2-800E-0080C76670C0}
static const GUID attrGuid4 =
{ 0xc45f05b6, 0x4d16, 0x11d2, { 0x80, 0xe, 0x0, 0x80, 0xc7, 0x66, 0x70, 0xc0 } };

// GUIDs for Consistency checking attributes

// {7707464B-4D9D-11d2-AF2D-00C04FB9624E}
static const GUID attrGuid5 =
{ 0x7707464b, 0x4d9d, 0x11d2, { 0xaf, 0x2d, 0x0, 0xc0, 0x4f, 0xb9, 0x62, 0x4e } };
// {7707464C-4D9D-11d2-AF2D-00C04FB9624E}
static const GUID attrGuid6 =
{ 0x7707464c, 0x4d9d, 0x11d2, { 0xaf, 0x2d, 0x0, 0xc0, 0x4f, 0xb9, 0x62, 0x4e } };

// {C45F05B5-4D16-11d2-800E-0080C76670C0}
static const GUID classGuid1 =
{ 0xc45f05b5, 0x4d16, 0x11d2, { 0x80, 0xe, 0x0, 0x80, 0xc7, 0x66, 0x70, 0xc0 } };

void __cdecl
main()
{
    HRESULT hr;
    IADs    *pRoot;
    VARIANT varDSRoot, varSchemaUpdate;
    TCHAR   dsPath[MAX_PATH];
    TCHAR   gpcPath[MAX_PATH];
    TCHAR   contPath[MAX_PATH];

    // Returned by CreateDSObject
    IDispatch           *pDisp;

    // Pointers to schema objects
    IDirectoryObject    *pSchema,
                        *pGpc;

    // Data structures for creating schema objects
    //
    // attribute values: these are unions and cannot be statically initialized.
    //
    ADSVALUE        cn,
                    singleValued,
                    oid,
                    syntax,
                    omSyntax,
                    ldapname,
                    idGuid,
                    objectClass,
                    objectClassCategory,
                    subClassOf,
                    defaultSecurityDesc,
                    defaultHidingValue,
                    mayContain[6];

    // ATTR_INFO for creating an attributeSchema object
    // Each ADS_ATTR_INFO describes one attribute of an object to be
    // stored in the DS.

    ADS_ATTR_INFO   attrArray[] = {
        {L"cn",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&cn,1},
        {L"isSingleValued",ADS_ATTR_UPDATE,ADSTYPE_BOOLEAN,&singleValued,1},
        {L"objectClass",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&objectClass,1},
        {L"attributeID",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&oid,1},
        {L"attributeSyntax",ADS_ATTR_UPDATE,ADSTYPE_INTEGER,&syntax,1},
        {L"oMSyntax",ADS_ATTR_UPDATE,ADSTYPE_INTEGER,&omSyntax,1},
        {L"lDAPDisplayName",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&ldapname,1},
        {L"schemaIdGUID",ADS_ATTR_UPDATE,ADSTYPE_OCTET_STRING,&idGuid,1},
    };

    // ATTR_INFO for creating a classSchema object

    ADS_ATTR_INFO   classArray[] = {
        {L"cn",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&cn,1},
        {L"objectClass",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&objectClass,1},
        {L"governsID",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&oid,1},
        {L"objectClassCategory",ADS_ATTR_UPDATE,ADSTYPE_INTEGER,&objectClassCategory,1},
        {L"schemaIdGUID",ADS_ATTR_UPDATE,ADSTYPE_OCTET_STRING,&idGuid,1},
        {L"defaultSecurityDescriptor",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&defaultSecurityDesc,1},
        {L"defaultHidingValue",ADS_ATTR_UPDATE,ADSTYPE_BOOLEAN,&defaultHidingValue,1},
        {L"subClassOf",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&subClassOf,1},
        {L"mayContain",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&mayContain[0],6},
    };

    // ATTR_INFO for adding attributes to Group Policy Container
    ADS_ATTR_INFO gpcUpdate[] = {
        {L"mayContain",ADS_ATTR_APPEND,ADSTYPE_CASE_IGNORE_STRING,&mayContain[0],2},
    };

    DWORD           dwAttrs;
    ULONG           iAttrsMod;

    hr = CoInitializeEx(NULL,COINIT_MULTITHREADED| COINIT_DISABLE_OLE1DDE);

    //  Get the name of the schema container for this domain.
    //  Read the Root DSE from the default DS,  which will be the DS for
    //  the local domain. This will get us the name of the schema container,
    //  which is stored in the "schemaNamingContext" operational attribute.

    hr = ADsGetObject(TEXT("LDAP://RootDSE"),
                      IID_IADs,
                      (void**)&pRoot);

    // Get IDirectoryObject on the root DSE as well, we will use this for
    // forcing a schema update later on...

    hr = pRoot->Get(L"schemaNamingContext",&varDSRoot);
    printf("\nDS Root:%S\n",varDSRoot.bstrVal);

    // ADsPath of the schema container

    _tcscpy(dsPath,TEXT("LDAP://"));
    _tcscat(dsPath,varDSRoot.bstrVal);

    // ADsPath of the Group Policy Container class-schema object
    _tcscpy(gpcPath,TEXT("LDAP://"));
    _tcscat(gpcPath,TEXT("CN=Group-Policy-Container,"));
    _tcscat(gpcPath,varDSRoot.bstrVal);

    // ADsPath of the Container class-schema object
    _tcscpy(contPath,TEXT("LDAP://"));
    _tcscat(contPath,TEXT("CN=Container,"));
    _tcscat(contPath,varDSRoot.bstrVal);

    // Done with variant
    hr = VariantClear(&varDSRoot);

    // Bind to the schema container and get the IDirectoryObject interface
    // on it.
    hr = ADsGetObject(dsPath,
                      IID_IDirectoryObject,
                      (void**)&pSchema);

    //**********************************************************************
    // Consistency-Child-Count
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    dwAttrs = sizeof(attrArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Consistency-Child-Count");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.161"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.9");              // 2.5.5.9 = Integer
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 2;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("consistencyChildCount");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid6);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid6;

    hr = pSchema->CreateDSObject(L"cn=Consistency-Child-Count",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Consistency-Child-Count failed."),hr);
        // return;
    } else {
        printf("\nConsistency-Child-Count Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**********************************************************************
    // Consistency-GUID
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Consistency-GUID");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.160"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.10");              // 2.5.5.10 = Octet String
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 4;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("consistencyGUID");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid5);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid5;

    hr = pSchema->CreateDSObject(L"cn=Consistency-GUID",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Consistency-GUID failed."),hr);
        // return;
    } else {
        printf("\nConsistency-GUID Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**********************************************************************
    // Course-Battle-Cruiser
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    dwAttrs = sizeof(attrArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Course-Battle-Cruiser");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.155"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.9");              // 2.5.5.9 = Integer
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 2;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("courseBattleCruiser");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid1);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid1;

    hr = pSchema->CreateDSObject(L"cn=Course-Battle-Cruiser",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Course-Battle-Cruiser failed."),hr);
        //return;
    } else {
        printf("\nCourse-Battle-Cruiser Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**********************************************************************
    // Speed-Battle-Cruiser
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    dwAttrs = sizeof(attrArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Speed-Battle-Cruiser");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.156"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.9");              // 2.5.5.9 = Integer
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 2;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("speedBattleCruiser");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid2);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid2;

    hr = pSchema->CreateDSObject(L"cn=Speed-Battle-Cruiser",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Speed-Battle-Cruiser failed."),hr);
        //return;
    } else {
        printf("\nSpeed-Battle-Cruiser Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**********************************************************************
    // Max-Payload-Battle-Cruiser
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    dwAttrs = sizeof(attrArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Max-Payload-Battle-Cruiser");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.157"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.9");              // 2.5.5.9 = Integer
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 2;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("maxPayloadBattleCruiser");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid3);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid3;

    hr = pSchema->CreateDSObject(L"cn=Max-Payload-Battle-Cruiser",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Max-Payload-Battle-Cruiser failed."),hr);
        // return;
    } else {
        printf("\nMax-Payload-Battle-Cruiser Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**********************************************************************
    // Allowed-Battle-Cruiser
    //**********************************************************************

    // Create a new attribute object. Set the values into the attribute
    // unions, then call the method to create the object.
    //

    dwAttrs = sizeof(attrArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Allowed-Battle-Cruiser");
    singleValued.dwType = ADSTYPE_BOOLEAN;
    singleValued.Boolean = VARIANT_TRUE;
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.4.7000.159"); // reserved test OID
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("attributeSchema");
    syntax.dwType = ADSTYPE_CASE_IGNORE_STRING;
    syntax.CaseIgnoreString = TEXT("2.5.5.8");              // 2.5.5.8 = Boolean
    omSyntax.dwType = ADSTYPE_INTEGER;
    omSyntax.Integer = 1;
    //
    // The ldap display name will be defaulted by the server and should
    // not be provided unless different than the name computed from the CN
    // attribute - the LDAP name is the CN with the hyphens removed and
    // case delimiting substituted. The initial character is always lowercased.
    // For this example we are providing an explicit LDAP display name to illustrate
    // how it is done.
    ldapname.dwType = ADSTYPE_CASE_IGNORE_STRING;
    ldapname.CaseIgnoreString = TEXT("allowedBattleCruiser");
    //
    // Schema-ID-Guid is provided by the server is the client does not
    // provide it. This is a good example of how to write an Octet String
    // to the DS.
    idGuid.dwType = ADSTYPE_OCTET_STRING;
    idGuid.OctetString.dwLength = sizeof(attrGuid4);
    idGuid.OctetString.lpValue = (LPBYTE)&attrGuid4;

    hr = pSchema->CreateDSObject(L"cn=Allowed-Battle-Cruiser",
                                 attrArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Attribute Allowed-Battle-Cruiser failed."),hr);
        //return;
    } else {
        printf("\nAllowed-Battle-Cruiser Attribute defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    // Force an update of the schema cache so we can create the class that
    // includes these attributes. We force a synchronous schema update by writing
    // the operational attribute "schemaUpdateNow" to the Root DSE.
    //
    varSchemaUpdate.vt = VT_I4;
    varSchemaUpdate.intVal = 1;
    hr = pRoot->Put(TEXT("schemaUpdateNow"),varSchemaUpdate);
    hr = pRoot->SetInfo();
    if (hr != NO_ERROR) {
        ReportError(TEXT("Force Schema Recalc failed."),hr);
    }

    //**********************************************************************
    // Policy-Parameters-Battle-Cruiser
    //**********************************************************************
    //
    // Create a new class object and add attributes
    // (including the new ones) to the class
    //
    cn.dwType = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString=TEXT("Policy-Parameters-Battle-Cruiser");
    objectClass.dwType = ADSTYPE_CASE_IGNORE_STRING;
    objectClass.CaseIgnoreString=TEXT("classSchema");
    oid.dwType = ADSTYPE_CASE_IGNORE_STRING;
    oid.CaseIgnoreString=TEXT("1.2.840.113556.1.5.7000.92"); // Reserved Test OID
    subClassOf.dwType = ADSTYPE_CASE_IGNORE_STRING;
    subClassOf.CaseIgnoreString = TEXT("serviceConnectionPoint");
    defaultSecurityDesc.dwType = ADSTYPE_CASE_IGNORE_STRING;
    defaultSecurityDesc.CaseIgnoreString = TEXT("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)(A;;RPLCLORC;;;AU)S:(AU;SAFA;WDWOSDDTWPCRCCDCSW;;;WD)");
    defaultHidingValue.dwType = ADSTYPE_BOOLEAN;
    defaultHidingValue.Boolean = -1;
    mayContain[0].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[0].CaseIgnoreString = TEXT("courseBattleCruiser");
    mayContain[1].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[1].CaseIgnoreString = TEXT("speedBattleCruiser");
    mayContain[2].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[2].CaseIgnoreString = TEXT("maxPayloadBattleCruiser");
    mayContain[3].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[3].CaseIgnoreString = TEXT("allowedBattleCruiser");
    mayContain[4].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[4].CaseIgnoreString = TEXT("consistencyGUID");
    mayContain[5].dwType = ADSTYPE_CASE_IGNORE_STRING;
    mayContain[5].CaseIgnoreString = TEXT("consistencyChildCount");

    // Object-Class-Category=
    // 88_CLASS           0
    // STRUCTURAL_CLASS   1
    // ABSTRACT_CLASS     2
    // AUXILIARY_CLASS    3
    objectClassCategory.dwType = ADSTYPE_INTEGER;
    objectClassCategory.Integer = 1;

    dwAttrs = sizeof(classArray)/sizeof(ADS_ATTR_INFO); // Figure out attribute count

    hr = pSchema->CreateDSObject(L"cn=Policy-Parameters-Battle-Cruiser",
                                 classArray,
                                 dwAttrs,
                                 &pDisp);
    if (hr != NO_ERROR) {
        ReportError(TEXT("Create Class Policy-Parameters-Battle-Cruiser failed."),hr);
    } else {
        printf("\nBattle Cruiser Class defined.\n");

    // We are not going to use the IDispatch interface returned by the
    // CreateDSObject call,  so release it now.
        pDisp->Release();
    }

    //**************************************************************************
    // Add the consistency attributes to Group-Policy-Container and Container
    //**************************************************************************

    hr = ADsGetObject(gpcPath,
                      IID_IDirectoryObject,
                      (void**)&pGpc);

    if (hr != NO_ERROR) {
        ReportError(TEXT("Read GPC class object failed."),hr);
        return;
    } else {
        printf("\nRetrieved GPC class object.\n");

        mayContain[0].dwType = ADSTYPE_CASE_IGNORE_STRING;
        mayContain[0].CaseIgnoreString = TEXT("consistencyGUID");
        mayContain[1].dwType = ADSTYPE_CASE_IGNORE_STRING;
        mayContain[1].CaseIgnoreString = TEXT("consistencyChildCount");
        hr = pGpc->SetObjectAttributes(gpcUpdate,1,&iAttrsMod);
        if (hr != NO_ERROR) {
            ReportError(TEXT("Update GPC Class object failed."),hr);
        } else {
            printf("\nUpdated GPC Class object.\n");
        }
    }

    // Done with class object
    pGpc->Release();

    //
    // Now apply the consistency attributes to Container. The ATTR_INFO
    // we need is already filled in, we just need to apply it.
    //
    hr = ADsGetObject(contPath,
                      IID_IDirectoryObject,
                      (void**)&pGpc);

    if (hr != NO_ERROR) {
        ReportError(TEXT("Read Container class object failed."),hr);
        return;
    } else {
        printf("\nRetrieved Container class object.\n");

        hr = pGpc->SetObjectAttributes(gpcUpdate,1,&iAttrsMod);
        if (hr != NO_ERROR) {
        ReportError(TEXT("Update Container Class object failed."),hr);
        } else {
            printf("\nUpdated Container Class object.\n");
        }
    }

    // Force an update of the schema cache so we can use the changes immediately.
    // We force a synchronous schema update by writing
    // the operational attribute "schemaUpdateNow" to the Root DSE.
    //
    varSchemaUpdate.vt = VT_I4;
    varSchemaUpdate.intVal = 1;
    hr = pRoot->Put(TEXT("schemaUpdateNow"),varSchemaUpdate);
    hr = pRoot->SetInfo();
    if (hr != NO_ERROR) {
        ReportError(TEXT("Force Schema Recalc failed."),hr);
    }

    // Done with Schema Container

    pSchema->Release();

    // Done with Root DSE
    pRoot->Release();

    //

    CoUninitialize();

}

// Simple error message reporter

void ReportError(TCHAR *pTxt,DWORD err)
{
    DWORD   dwStatus;
    TCHAR   *pBuf;

    dwStatus = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM|
                             FORMAT_MESSAGE_ALLOCATE_BUFFER|
                             FORMAT_MESSAGE_IGNORE_INSERTS,
                             NULL,
                             err,
                             LANG_NEUTRAL,
                             (USHORT*)&pBuf,
                             64,
                             NULL);
    if (dwStatus != 0) {
        _tprintf(TEXT("%s %s"),pTxt,pBuf);
        LocalFree(pBuf);
    } else {
        _tprintf(TEXT("%s %X\n"),pTxt,err);
    }
}


Managing Users

User accounts are created and stored as objects in Active Directory™. These user objects represent users and computers. This section defines what users are and how they are used, and explains how to programmatically manage users in Active Directory. This section discusses the following topics:

  • Users in Active Directory
  • Security Principals
  • What is a User?
  • Reading a user object
  • Creating a user
  • Deleting a user
  • Enumerating users
  • Querying for users
  • Moving users
  • Managing users on member servers and Windows 2000 Professional


Users in Active Directory

In Windows® 2000, Active Directory™ is a generalized directory service that includes storage of domain, user, user group, and security information.

In Windows NT® 4.0 and earlier, you used the Net functions (such as NetUserAdd, NetUserEnum, NetUserDel, and so on) to manage users, user groups, and other network items. With Windows 2000, ADSI provides uniform and secure access to these items and their properties. Note that ADSI provides a Windows NT 4.0 provider that enables you to use ADSI to manage user, user groups, and computers on Windows NT 4.0 systems. There are also providers for Microsoft® Exchange 5.5, Microsoft Internet Information Server, Novell NetWare® Directory Services (NDS) and Novell NetWare® 3. This means a single set of standardized methods for managing users and user groups for Windows NT, NDS, and NetWare 3.

In addition, Windows 2000 is a multi-master directory. This means that changes to users, user groups, and other information stored in the directory can be made at any domain controller. On Windows 2000, you no longer need to locate the primary domain controller (PDC) and make user and user group changes on the PDC.

Windows 2000 also introduces a new hierarchical namespace within a domain called an organizational unit (OU). An OU can contain computers, users, user groups, and other network objects. Usually, an OU is used for the purpose of grouping things for administrative purposes, such as delegating administrative rights and assigning policies to the group as a single unit.

Domains, OUs, users, user groups, computers, and other network items are stored as objects in Active Directory. In Windows 2000, you still add users, user groups, and computers to a domain. However, you now have the option of adding these objects to an OU container or any other type of container that the object you want to add defines in its classSchema object's Poss-Superiors attribute (this is a property on an object's classSchema object and this property restricts what types of objects can contain that object).


Security Principals

In Windows® 2000, a security principal is a user, group, or computer. an entity that the security system recognizes. This includes human users as well as autonomous processes. Strictly speaking, the security system cannot tell the difference between users who are logged in and processes running on the computer. It sees both as security principals with security principal names.

Users, groups, and computers are created and stored as objects in Active Directory™. There are also well-known security principals that represent special identities defined by the Windows 2000 security system, such as Everyone, Local System, Principal Self, Authenticated User, Creator Owner, and so on. Objects representing the well-known security principals, such as Anonymous Logon, are stored in the WellKnown Security Principals container beneath the Configuration container.


What is a User?

User accounts are created and stored as objects in Active Directory™. User accounts can be used by human users or programs such as Win32 services use to log on to a computer. When a user logs on, the system verifies the user's password by comparing it with information stored in the user's user object in Active Directory. If the password is authenticated (that is, the password presented matches the password stored in the user object), the system produces an access token. An access token is an object that describes the security context of a process or thread. The information in a token includes the security identity and group memberships of the user account associated with the process or thread. Every process executed on behalf of this user has a copy of this access token.

Each user or application that accesses resources in a Windows® 2000 domain must have an account in Active Directory. Windows 2000 uses this user account to verify that the user or application has permission to use a resource.

A user account can be used to do the following:

  • Enable human users to log on to a computer and to access resources based on that user account's identity.
  • Enable programs and services to run under a specific security context.
  • Manage user access to shared resources such as Active Directory objects and their properties, network shares, files, directories, printer queues, and so on.

Groups can contain members, which are references to users and other groups. Groups can also be used to control access to shared resources. When assigning permissions for resources (file shares, printers, and so on), administrators should assign those permissions to a group rather than to the individual users. The permissions are assigned once to the group, instead of several times to each individual user. This helps simplify the maintenance and administration of a network.

Users vs. Contacts

[This is preliminary documentation and subject to change.]

Both users and contacts can be used to represent human users. However, a user is a security principal; a contact is not.

A user can be used to enable a human user to log on and access shared resources.

A contact is used only for distribution list and e-mail purposes. However, a contact can contain most of the information stored in a user object such as address, phone numbers, and so on — since both user and contact are derived from the person classSchema object. A contact has no security context; therefore, a contact cannot be used to control access to shared resources and cannot be used to log on to a computer.

Users vs. Computers

[This is preliminary documentation and subject to change.]

The computer object class inherits from the user object class. A computer object represents a computer; however, the computer and the computer's local services often require access to the network and shared resources. When the computer accesses shared resources (not the user logged on to the computer), it needs an access token just as a human user logged on as a user does. When a computer accesses the network, it uses an access token containing the security identifier for the computer's computer account and the groups that account is a member of.

A service can run in the context of LocalSystem or a specific service account. On computers running Windows 2000, a service that runs in the context of the LocalSystem account uses the credentials of the computer.


Reading a User Object

To read the properties of a user object, bind to the user object and use the methods provided by the IADs, IADsPropertyList, or IADsUser interfaces. In C/C++, you can also use IDirectoryObject to retrieve specific properties. For information on reading properties of an object, see Reading Properties for an Active Directory Object.


Binding to a User Object

To bind to a user object, specify the user object's distinguished name or its objectGUID as you would with any other object.

For information about binding, see Binding.


User Object Properties

A user object has a number of properties. This section documents the key properties that are used by the Windows® 2000 operating system, administrative tools, and the Windows Address Book (WAB). It does not describe all properties (many are not used) on the user object.

Some properties are stored in the directory (such as cn, nTSecurityDescriptor, objectGUID, and so on) and replicated to all domain controllers within a domain. A subset of these properties is also replicated to the global catalog.

Non-replicated properties are stored on each domain controller but are not replicated elsewhere (such as badPwdCount, lastLogon, lastLogoff, and so on). The non-replicated properties are properties that pertain to a particular domain controller. For example, lastLogon is the last date and time that the user's network logon was validated by the particular domain controller that is returning the property.

A user object also has constructed properties that are not stored in the directory but are calculated by the domain controller (such as canonicalName, distinguishedName, allowedAttributes, ADsPath, and so on). Note that distinguishedName and ADsPath are not defined in the schema.

Properties for user objects fall into the following categories:

  • Base object. This category covers properties required on all directory objects, such as objectClass, nTSecurityDescriptor, and so on.
  • Naming/Identity. This category covers properties that are referring to or identifying the object, such as distinguishedName, objectGUID, objectSid, and so on.
  • Security. This category covers properties for logon and access control purposes.
  • Address book. This category covers properties for e-mail and general information about the user.
  • Application-specific information. This category covers user-specific configuration information for specific applications such as dial-up networking.

For a table of all default properties defined in the user class, see Data Types for User Object Properties.


Naming Properties

As with other directory objects, a user object has names in the form of cn, name, distinguishedName, and objectGUID properties. Note that a user's relative distinguished name (RDN) is the value of the cn property. These properties can be viewed and managed by the Active Directory™ User and Computers snap-in.

Since a user object is a security principal, it has the following additional naming properties:

userPrincipalName (User-Principal-Name)
The userPrincipalName is a single-valued and indexed property that is a string that specifies the user principal name (UPN) of the user. The UPN is an Internet-style login name for the user based on the Internet standard RFC 822. The UPN is shorter than the distinguished name and easier to remember. By convention, this should map to the user's e-mail name. The point of the UPN is to consolidate the e-mail and logon namespaces so that the user need only remember a single name.

The UPN is the preferred logon name for Windows 2000 users. Users should be using their UPNs to log on to the domain. At logon time, a UPN is validated first by searching the local domain, then the global catalog. Failure to find the UPN in the local domain or the GC results in rejection of the UPN.

The UPN can be assigned, but is not required, when the user account is created. Once assigned, the UPN is unaffected by changes to other properties of the user object (for example, if the user is renamed or moved) or changes to the domains in the tree (for example, if a parent domain was renamed or a domain was moved).Thus, a user can keep the same login name, although the directory may be radically restructured. Note that the UPN can be administratively changed at any time.

Note that the UPN is a string property that the can contain any string value. However, the following scheme is recommended:

The user principal name has two parts: the UPN prefix (the user account name) and the UPN suffix (a DNS domain name). The parts are joined together by the @ (at sign) symbol to make the complete UPN. For example, the user Someone who has an account in the Arcadiabay domain would have a UPN of someone@arcadiabay.com.

The UPN must be unique among all security principal objects within the directory forest. By default (that is, for the built-in user accounts and user accounts created using the Active Directory Users and Computers snap-in), the UPN can consist of any name for the user (such as the sAMAccountName property of the user) and the domain tree name to which the user belongs in the following form:

Name@treeName

The treeName is the domain name system (DNS) name of a domain, but is not required to be the name of the domain containing the user. However, the treeName portion of the UPN must be the name of a domain in the current forest or an alternate name listed in the upnSuffixes property of the Partitions container within the Configuration container. You can add or remove UPN suffixes by modifying the upnSuffixes property (or by choosing Properties for the root node of the Active Directory Domains and Trusts and modifying the UPN suffixes on the UPN Suffixes tab). Usually, the treeName is the name of the first domain in the first tree of the forest. In most cases, this domain name is the domain name registered as the enterprise domain on the Internet. The tree name is stored in a property ("treeName") stored on the domainDNS object.

When creating a new user object, you should check the local domain and the global catalog for the proposed name to ensure it does not already exist.

objectGUID (Object-GUID)
The objectGUID property is a single-valued property that is the unique identifier for the object. This property is a Globally Unique Identifier (GUID). When an object is created in the directory, Active Directory generates a GUID and assigns it to the object's objectGUID property. The GUID is unique across the enterprise and anywhere else.

The objectGUID is a 128-bit GUID structure stored as an OctetString.

Because an object's distinguished name changes if the object is renamed or moved, the domain name is not a reliable identifier for an object. In Active Directory, an object's objectGUID property is never changed, even if the object is renamed or moved to different places. Note that you can retrieve the string form of the objectGUID using the IADs::get_GUID method.

sAMAccountName (SAM-Account-Name)
The sAMAccountName property is a single-valued property that is the logon name used to support clients and servers from a previous version of Windows (such as Windows NT® 4.0 and earlier, Windows 95, Windows 98, and LAN Manager). Note that the sAMAccountName should be less than 20 characters to support these clients and servers.

The sAMAccountName must be unique among all security principal objects within the domain.

You should query for the new name against the domain to verify that the sAMAccountName is unique in the domain.

The sAMAccountName must be unique among all security principal objects within a domain container.

objectSid (Object-Sid)
The objectSid property is a single-valued property that specifies the security identifier (SID) of the user. The SID is a unique value used to identify the user as a security principal. It is a binary value that is set by the system when the user is created.

Each user has a unique SID issued by a Windows 2000 domain and stored in objectSid property of the user object in the directory. Each time a user logs on, the system retrieves the user's SID from the directory and places it in the user's access token. The user's SID is also used to retrieve the SIDs for the groups of which the user is a member and places them in the user's access token. The system uses the SIDs in the user's access token to identify the user and his/her group memberships in all subsequent interactions with Windows NT security.

When a SID has been used as the unique identifier for a user or group, it cannot ever be used again to identify another user or group.

sIDHistory (SIDHistory)
The sIDHistory property is a multi-valued property that contains previous SIDs used for the user object if the user was moved from another domain. Whenever a user is moved from one domain to another, a new SID is created and that new SID becomes the objectSID. The previous SID is added to the sIDHistory property which contains the SIDs from the user's previous domain moves.


Security Properties

In addition to naming properties for user objects (objectGUID, objectSid, cn, distinguishedName, and so on), there are other security properties that are used for logon, network access, and access control. These properties are used by the Windows 2000 security system. These properties can be viewed and managed by the Active Directory User and Computers snap-in.

accountExpires
The accountExpires property specifies when the account will expire. This value is stored as a large integer that represents the number of seconds elapsed since 00:00:00, January 1, 1970. A value of TIMEQ_FOREVER indicates that the account never expires.

This value is defined in LMACCESS.H.

altSecurityIdentities
The altSecurityIdentities property is a multi-valued property that contains mappings for X.509 certificates or external Kerberos user accounts to this user for the purpose of authentication. Various security packages (including Public Key authentication package and Kerberos) will use this information to authenticate users when they present the alternative form of identification (such as certificate, Unix Kerberos ticket, and so on.) and build an Windows 2000 token based on the corresponding user account such that they can access system resources.

For X.509 certificates, the values should be the Issuer and Subject names in 509v3 certificates (issued by an external public Certificate Authority) that map to the user account. that can be used to find an account for authentication. The SSL (schannel) package uses the following syntax: X509:<somecertinfotype>somecertinfo. For example, the following value specifies the issuer DN "<I>" with the DN C=US,O=InternetCA,CN=APublicCertificateAuthority and the subject DN "<S>" with the DN C=US,O=ArcadiaBay,OU=FOO,CN=John Smith:

X509:<I>C=US,O=InternetCA,CN=APublicCertificateAuthority<S>C=US,O=ArcadiaBay,OU=FOO,CN=John Smith

Note that <I> or <I> and <S> are supported. Having only <S> is not supported. Applications should not modify the values within <I> or <S> because partial DN matching is not supported.

For external Kerberos accounts, the values should be the Kerberos account name. The Kerberos package uses the following syntax: Kerberos:MITaccountname. For example, the following is the value for an account at arcadiabay.com:

Kerberos:John.Doe@arcadiabay.com
badPasswordTime (Non-replicated)
The badPasswordTime property specifies when the last time the user tried to log onto the account using an incorrect password. This value is stored as a large integer that represents the number of seconds elapsed since 00:00:00, January 1, 1970. This property is maintained separately on each domain controller in the domain. A value of zero means that the last bad password time is unknown. To get an accurate value for the user's last bad password time in the domain, each domain controller in the domain must be queried and the largest value should be used.
badPwdCount (Non-replicated)
The badPwdCount property specifies the number of times the user tried to log on to the account using an incorrect password. This property is maintained separately on each domain controller in the domain. A value of 0 indicates that the value is unknown. To get an accurate value for the user's total bad password attempts in the domain, each domain controller in the domain must be queried and the sum of the values should be used.
codePage
The codePage property specifies the code page for the user's language of choice. This value is not used by Windows 2000.
countryCode
The countryCode property specifies the country code for the user's language of choice. This value is not used by Windows 2000.
homeDirectory
The homeDirectory property specifies the path of the home directory for the user. The string can be null.

If homeDrive is set and specifies a drive letter, homeDirectory should be a UNC path. The path must be a network UNC path of the form \\server\share\directory. This value can be a null string.

If homeDrive is not set, homeDirectory should be a local path (such as C:\mylocaldir).

homeDrive
The homeDrive property specifies the drive letter to which to map the UNC path specified by homeDirectory. The drive letter must be specified in the following form:

driveletter:

where driveletter is the letter of the drive to map. For example:

Z:

If this property is not set, the homeDirectory should be a local path (such as C:\mylocaldir).

lastLogoff (Non-replicated)
The lastLogoff property specifies when the last logoff occurred. This value is stored as a large integer that represents the number of seconds elapsed since 00:00:00, January 1, 1970. This property is maintained separately on each domain controller in the domain. A value of zero means that the last logoff time is unknown. To get an accurate value for the user's last logoff in the domain, each domain controller in the domain must be queried and the largest value should be used.
lastLogon (Non-replicated)
The lastLogon property specifies when the last logon occurred. This value is stored as a large integer that represents the number of seconds elapsed since 00:00:00, January 1, 1970. This property is maintained separately on each domain controller in the domain. A value of zero means that the last logon time is unknown. To get an accurate value for the user's last logon in the domain, each domain controller in the domain must be queried and the largest value should be used.
lmPwdHistory
The lmPwdHistory property is the password history of the user in LAN Manager (LM) one-way format (OWF). The LM OWF is used for compatibility with LAN Manager 2.x clients, Windows 95, and Windows 98. This property is used only by operating system. Note that you cannot derive the clear password back from the OWF form of the password.
logonCount (Non-replicated)
The logonCount property counts the number of successful times the user tried to log on to this account. This property is maintained separately on each domain controller in the domain. A value of 0 indicates that the value is unknown. To get an accurate value for the user's total number of successful logon attempts in the domain, each domain controller in the domain must be queried and the sum of the values should be used.
mail (E-mail-Addresses)
The mail property is a single-valued property that contains the SMTP address for the user (such as john@arcadiabay.com).
memberOf
The memberOf property is a multi-valued property that contains groups of which the user is a direct member, depending on the domain controller (DC) from which this property is retrieved:
  • At a DC for the domain containing the user, memberOf for the user is complete with respect to membership for groups in that domain; however, memberOf does not contain the user's membership in domain local and global groups in other domains.
  • At a GC server, memberOf for the user is complete with respect to all universal group memberships.

If both conditions are true about the DC, both sets of information are contained in memberOf.

Note that this property lists the groups that contain the user in their member property—it does not contain the recursive list of nested predecessors. For example, if user O is a member of group C and group B and group B were nested in group A, the membersOf property of user O would list group C and group B but not group A.

This property is not stored—it is a computed back-link attribute.

ntPwdHistory
The ntPwdHistory property is the password history of the user in Windows NT (NT) one-way format (OWF). Windows® 2000 uses the NT OWF. This property is used only by operating system. Note that you cannot derive the clear password back from the OWF form of the password.
otherMailbox
The otherMailbox property is a multi-valued property containing other additional mail addresses in a form such as CCMAIL: JohnDoe.
PasswordExpirationDate
The password expiration date is not a property on the user object. It is a calculated value based on the sum of pwdLastSet for the user and maxPwdAge of the user's domain. To get the password expiration date, call the IADsUser::get_PasswordExpirationDate method. You cannot modify this property for a user; instead, call IADsDomain::put_MaxPasswordAge method to change the setting for the domain.
primaryGroupID
The primaryGroupID property is a single-valued property containing the relative identifier (RID) for the primary group of the user. By default, this is the RID for the Domain Users group. This property is not used in the context of the Active Directory.
profilePath
The profilePath property specifies a path to the user's profile. This value can be a null string, a local absolute path, or a UNC path.
pwdLastSet
The pwdLastSet property specifies when the user last set the password. This value is stored as a large integer that represents the number of seconds elapsed since 00:00:00, January 1, 1970.

The system uses the value of this property and the maxPwdAge property of the domain containing the user object to calculate the password expiration date (sum of pwdLastSet for the user and maxPwdAge of the user's domain).

controls whether the user must change the password the next time the user logs on.

Default is 0. Zero(0) means the user must change the password at next logon. The value -1 means the user does not need to change the password at next logon. The system sets this value to -1 after user has set the password.

sAMAccountType
The sAMAccountType property specifies an integer that represents the account type. This is set by the operating system when the object is created.
scriptPath
The scriptPath property specifies the path of the user's logon script, .CMD, .EXE, or .BAT file. The string can be null.
unicodePwd
The unicodePwd property is the password for the user.

For setting the password of the user, you should use the IADsUser::ChangePassword method (if your script or application is allowing the user to change his/her own password) or IADsUser::SetPassword method (if your script or application is allowing an administrator to reset a password).

The password of the user in Windows NT (NT) one-way format (OWF). Windows® 2000 uses the NT OWF. This property is used only by operating system. Note that you cannot derive the clear password back from the OWF form of the password.

userAccountControl
The userAccountControl property specifies flags that control password, lockout, disable/enable, script, and home directory behavior for the user. This property also contains a flag that indicates the account type of the object. The user object usually has the UF_NORMAL_ACCOUNT set.

The flags are defined in LMACCESS.H.
Value | Meaning
UF_SCRIPT | The logon script executed. This value must be set for LAN Manager 2.0 or Windows NT.
UF_ACCOUNTDISABLE | The user's account is disabled.
UF_HOMEDIR_REQUIRED | The home directory is required. This value is ignored in Windows NT and Windows 2000.
UF_PASSWD_NOTREQD | No password is required.
UF_PASSWD_CANT_CHANGE | The user cannot change the password.
UF_LOCKOUT | The account is currently locked out. This value can be cleared to unlock a previously locked account. This value cannot be used to lock a previously locked account.
UF_DONT_EXPIRE_PASSWD | Represents the password, which should never expire on the account.

The following values describe the account type. Only one value can be set. You cannot change the account type.
Value | Meaning
UF_NORMAL_ACCOUNT | This is a default account type that represents a typical user
UF_TEMP_DUPLICATE_ACCOUNT | This is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. The User Manager refers to this account type as a local user account.
UF_WORKSTATION_TRUST_ACCOUNT | This is a computer account for a Windows NT Workstation/Windows 2000 Professional or Windows NT Server/Windows 2000 Server that is a member of this domain.
UF_SERVER_TRUST_ACCOUNT | This is a computer account for a Windows NT Backup Domain Controller that is a member of this domain.
UF_INTERDOMAIN_TRUST_ACCOUNT | This is a permit to trust account for a Windows NT domain that trusts other domains.

userCertificate (X509-Cert)
The userCertificate property is a multi-valued property that contains the DER-encoded X509v3 certificates issued to the user. Note that this property contains the public key certificates issued to this user by Microsoft® Certificate Service.
userSharedFolder
The userSharedFolder property specifies a UNC path to the user's shared documents folder. The path must be a network UNC path of the form \\server\share\directory. This value can be a null string.
userWorkstations
The userWorkstations property is a single-valued property containing the NetBIOS names of the computers running Windows NT Workstation/Windows 2000 Professional from which the user can log on. Each NetBIOS name is separated by a comma. The NetBIOS name of a computer is the sAMAccountName property of a computer object.

If there are no values set, it indicates that there is no restriction. To disable logons from all computers running Windows NT Workstation/Windows 2000 Professional to this account, set the UF_ACCOUNTDISABLE value in userAccountControl property.

This value is defined in LMACCESS.H.

maxStorage
The maxStorage property specifies the maximum amount of disk space the user can use. Use the value specified in USER_MAXSTORAGE_UNLIMITED to use all available disk space.

This value is defined in LMACCESS.H.


Address Book Properties

The address book properties are used to provide supplementary identification and information for a user. Their content is defined by the user and the user's organization. These properties can be viewed and managed by the Active Directory User and Computers snap-in or the Windows Address Book (WAB).

c (Country-Name)
The Country-Name property is the country in the user's address.

The country is represented as the 2-character country code based on ISO-3166. For the country codes, see Values for countryCode.

co (Text-Country)
The co property is the country the user is located in.
notes (Comment)
The notes property is a comment. This string can be a null string.
department (Department)
The department property is a single-valued property that contains the name for the department in which the user works.
description (Description)
The description property is a single-valued property that contains the description to display for the user.
displayName (Display-Name)
The displayName property is the name displayed in the address book for a particular user. This is usually the combination of the users first name, middle initial, and last name.
directReports (Reports)
The directReports property is a multi-valued property that contains the list of users that directly report to the user. The users listed as reports are those that have the property manager property set to this user. Each item in the list is a linked reference to the object that represents the user; therefore, Active Directory automatically updates the directReports property when a user's manager property adds or removes this user as a manager. The items are represented as distinguished names.
facsimileTelephoneNumber (Facsimile-Telephone-Number)
The facsimileTelephoneNumber property is a single-valued property that contains telephone number of the user's business fax machine.
givenName (Given-Name)
The givenName property is a single-valued property that contains the given name (first name) of the user.
homePhone (Phone-Home-Primary)
The homePhone property is a single-valued property containing the primary home telephone number for the user.
initials (Initials)
The initials property is single-valued property containing the initials for parts of the user's full name. This may be used as the middle initial in the Windows Address Book.
ipPhone (Phone-Ip-Primary)
The ipPhone property is used by Telephony.
l (Locality-Name)
The l property is a single-valued property containing the locality, such as the town or city, in the user's address.
managedObjects (Managed-Objects)
The managedObjects property is a multi-valued property that contains the list of objects that are managed by the user. The objects listed are those that have the property managedBy property set to this user. Each item in the list is a linked reference to the managed object; therefore, Active Directory automatically updates the managedObjects property when an object's managedBy property adds or removes this user as its manager. The items are represented as distinguished names.
manager (Manager)
The manager property is a single-valued property that contains the user who is the user's manager. The manager's user object contains a directReports property that contains references to all user objects that have their manager properties set to the manager's user object. See directReports.
mobile (Phone-Mobile-Primary)
The mobile property is a single-valued property containing the primary cellular telephone number for the user.
otherFacsimileTelephoneNumber (Phone-Fax-Other)
The otherFacsimileTelephoneNumber property is a multi-valued property that contains telephone numbers of alternate fax machines for the user.
otherIpPhone (Phone-Ip-Other)
The otherIpPhone property is used by Telephony.
otherMobile (Phone-Mobile-Other)
The otherMobile property is a multi-valued property that contains alternate cellular telephone numbers for the user.
otherPager (Phone-Pager-Other)
The otherPager property is a multi-valued property that contains alternate pager telephone numbers for the user.
otherTelephone (Phone-Office-Other)
The otherTelephone property is a multi-valued property that contains alternate business telephone numbers for the user.
pager (Phone-Pager-Primary)
The pager property is a single-valued property containing the primary pager telephone number for the user.
physicalDeliveryOfficeName (Physical-Delivery-Office-Name)
The physicalDeliveryOfficeName property is a single-valued property that contains the office location in the user's place of business.
postalAddress (Postal-Address)
The postalAddress property is a single-valued property containing the user's postal address.
postalCode (Postal-Code)
The postalCode property is a single-valued property containing postal code for the user's postal address. The postal code is specific to the user's country. In the United States of America, this property contains the ZIP code.
postOfficeBox (Post-Office-Box)
The postOfficeBox property is a single-valued property containing the number or identifier of the user's post office box.
sn (Surname)
The sn property is a single-valued property that specifies the user's surname (family name or last name).
st (State-Or-Province-Name)
The st property is the state or province in the user's address.
street (Street-Address)
The street property is a single-valued property that contains the street address of the user's place of business.
telephoneNumber (Telephone-Number)
The telephoneNumber property is a single-valued property that contains the primary telephone number of the user's place of business.
title (Title)
The title property is a single-valued property that contains the user's job title. This property is commonly used to indicate the formal job title, such as Senior Programmer, rather than occupational class, such as programmer. It is not typically used for "suffix" titles such as Esq. or DDS.

Examples: Managing Director, Programmer II, Associate Professor, and Development Lead.

url (WWW-Page-Other)
The url property is a multi-valued property that contains the URLs for the user's alternate web pages.
wWWHomePage (WWW-Home-Page)
The wWWHomePage property is a single-valued property that contains the URL for the user's primary web page.


Reading User Object Properties

For general information about reading properties, see Reading Properties for an Active Directory Object.

All objects have properties; however, the user object has three types of properties that are stored differently:

Domain-replicated, stored properties
Some properties are stored in the directory (such as cn, nTSecurityDescriptor, objectGUID, and so on) and replicated to all domain controllers within a domain. A subset of these properties is also replicated to the global catalog. If you enumerate properties on user object from the global catalog, only the properties that are replicated to the global catalog are returned. Some properties are also indexed—including an indexed property in a query improves the performance of a query.

These properties can be retrieved using the IADs::Get method, IADsProperty* methods, IDirectoryObject::GetObjectAttributes method, and IDirectorySearch methods.

Non-replicated, locally stored properties
Non-replicated properties are stored on each domain controller but are not replicated elsewhere (such as badPwdCount, lastLogon, lastLogoff, and so on). The non-replicated properties are properties that pertain to a particular domain controller. For example, lastLogon is the last date/time that the user's network logon was validated by the particular domain controller that is returning the property. These properties can be retrieved in the same way as the domain-wide properties described previously. However, for these properties, each domain controller stores only values that pertain to that particular domain controller. For example, if you wanted to get the last time a user logged on to the domain, you would have to read the lastLogon property for the user at every domain controller in the domain and find latest time.
Non-stored, constructed properties
A user object also has constructed properties that are not stored in the directory but are calculated by the domain controller (such as canonicalName, distinguishedName, allowedAttributes, ADsPath, and so on). Note that distinguishedName and ADsPath are not defined in the schema. All the constructed properties can be retrieved by IDirectoryObject and IDirectorySearch methods. Most are automatically retrieved and cached with a IADs::GetInfo call on the user object (Note IADs::Get does an implicit IADs::GetInfo call if the cache is empty). However, some constructed properties are not and therefore require a IADs::GetInfoEx call to explicitly retrieve them. For example, canonicalName is not retrieved with a IADs::GetInfo call and therefore IADs::Get will return a E_ADS_PROPERTY_NOT_FOUND error and enumeration of all properties using IADsPropertyList will not include the canonicalName property.


Determining Which Properties Are Non-Replicated, Constructed, Global Catalog, and Indexed

The systemFlags property of each property's attributeSchema object contains flags that define whether a property is constructed or non-replicated:

#define ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED         (0x00000001)
#define ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED         (0x00000004)

For example, the following query string finds the attributeSchema objects representing on-replicated properties:

(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804:=ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED))

The searchFlags property of each property's attributeSchema object defines whether a property is indexed (indexed has a value of 1, non-indexed is 0). For example, the following query string finds the attributeSchema objects representing indexed properties:

(&(objectCategory=attributeSchema)(searchFlags=1))

The isMemberOfPartialAttributeSet property of each property's attributeSchema object defines whether a property is replicated to the global catalog (in the global catalog that has a value of TRUE, not in the global catalog that is FALSE). For example, the following query string finds the attributeSchema objects representing global catalog properties:

(&(objectCategory=attributeSchema)(isMemberOfPartialAttributeSet=TRUE))


Example Code for Reading Properties

The following code fragments enumerate the properties of the specified user in the current domain, by searching for the user and then using IADsPropertyList to enumerate its properties (note how time/date values as large integers are handled and how octet strings for objectSID and objectGUID are handled.).

[C++]
//For the pow function to calculate powers of 2
#include <math.h>
#include <wchar.h>
#include <objbase.h>
#include <activeds.h>

//Make sure you define UNICODE
//Need to define version 5 for Windows 2000
#define _WIN32_WINNT 0x0500
//For SID conversion API.
#include <sddl.h>

HRESULT GetUserProperties(IADs * pObj);

HRESULT EnumeratePropertyValue(
        IADsPropertyEntry *pEntry
        );

HRESULT FindUserByName(IDirectorySearch *pSearchBase, //Container to search
                LPOLESTR szFindUser, //Name of user to find.
                IADs **ppUser); //Return a pointer to the user

void wmain( int argc, wchar_t *argv[ ])
{

//Handle the command line arguments.
LPOLESTR pszBuffer = new OLECHAR[MAX_PATH*2];
if (argv[1] == NULL)
{
    wprintf(L"This program finds a user in the current Window 2000 domain\n");
    wprintf(L"and displays its properties.\n");
    wprintf(L"Enter Common Name of the user to find:");
    _getws(pszBuffer);
}
else
    wcscpy(pszBuffer, argv[1]);
//if empty string, exit.
if (0==wcscmp(L"", pszBuffer))
    return;

wprintf(L"\nFinding user: %s...\n",pszBuffer);

//Intialize COM
CoInitialize(NULL);
HRESULT hr = S_OK;
//Get rootDSE and the domain container's DN.
IADs *pObject = NULL;
IDirectorySearch *pDS = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
            NULL,
            NULL,
            ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
            IID_IADs,
            (void**)&pObject);
if (FAILED(hr))
{
    wprintf(L"Not Found. Could not bind to the domain.\n");
    if (pObject)
        pObject->Release();
    return;
}

hr = pObject->Get(L"defaultNamingContext",&var);
if (SUCCEEDED(hr))
{
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    VariantClear(&var);
    if (pObject)
    {
        pObject->Release();
        pObject = NULL;
    }
    //Bind to the root of the current domain.
    hr = ADsOpenObject(szPath,
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IDirectorySearch,
                (void**)&pDS);
    if (SUCCEEDED(hr))
    {
        hr =  FindUserByName(pDS, //Container to search
                    pszBuffer, //Name of user to find.
                    &pObject); //Return a pointer to the user
        if (SUCCEEDED(hr))
        {
            wprintf (L"----------------------------------------------\n");
            wprintf (L"--------Call GetUserProperties-----------\n");
            hr = GetUserProperties(pObject);
            wprintf (L"GetUserProperties HR: %x\n", hr);
        }
        else
        {
            wprintf(L"User \"%s\" not Found.\n",pszBuffer);
            wprintf (L"FindUserByName failed with the following HR: %x\n", hr);
        }
        if (pObject)
            pObject->Release();
    }

    if (pDS)
        pDS->Release();
}

//uninitialize COM
CoUninitialize();

    return;
}

HRESULT GetUserProperties(IADs * pObj)
{
    HRESULT hr = E_FAIL;
    LPOLESTR szDSPath = new OLECHAR [MAX_PATH];
    IADsPropertyList *pObjProps = NULL;
    IADsPropertyEntry *pEntry = NULL;
    VARIANT var;
    BSTR szString;
    long lCount = 0L;
    long lCountTotal = 0L;
    long lPType = 0L;
    if (!pObj)
        return E_INVALIDARG;
    //Call GetInfo to load all properties for the object into the cache.
    //Must do this because IADsPropertyList methods read from the cache.
    hr = pObj->GetInfo();
    if (SUCCEEDED(hr))
    {
        //QI for an IADsPropertyList pointer.
        hr = pObj->QueryInterface(IID_IADsPropertyList, (void**)&pObjProps);
        if (SUCCEEDED(hr))
        {
            //Enumerate the properties of the object
            hr = pObjProps->get_PropertyCount(&lCountTotal);
            wprintf(L"Property Count: %d\n",lCountTotal);
            hr = pObjProps->Next(&var);
            if (SUCCEEDED(hr))
            {
                lCount = 1L;
                while (hr==S_OK)
                {
                    if (var.vt==VT_DISPATCH)
                    {
                        hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
(void**)&pEntry);
                        if (SUCCEEDED(hr))
                        {
                            hr = pEntry->get_Name(&szString);
                            wprintf(L"%s: ",szString);
                            SysFreeString(szString);
                            hr = pEntry->get_ADsType(&lPType);
                            if (lPType != ADSTYPE_INVALID)
                            {
                                hr = EnumeratePropertyValue(pEntry);
                                if(FAILED(hr))
                                    printf("EnumeratePropertyValue failed. hr:
%x\n",hr);
                            }
                            else
                            {
                            wprintf(L"Invalid type\n");
                            }
                            }
                            else
                            {
                            printf("IADsPropertyEntry QueryInterface call failed. hr:
%x\n",hr);
                        }
                        //Clean up
                        if (pEntry)
                            pEntry->Release();
                    }
                    else
                    {
                        printf("Unexpected returned type for VARIANT: %d",var.vt);
                    }
                    VariantClear(&var);
                    hr = pObjProps->Next(&var);
                    if (SUCCEEDED(hr))
                    {
                        lCount++;
                    }
                }
            }
        }
        wprintf(L"Total properties retrieved: %d\n",lCount);
        //Clean up
        if (pObjProps)
            pObjProps->Release();
    }
    //Return success if all properties were retrieved.
    if (lCountTotal==lCount)
        hr=S_OK;
    return hr;
}

HRESULT EnumeratePropertyValue(
          IADsPropertyEntry *pEntry
          )
{
HRESULT hr = E_FAIL;
IADsPropertyValue *pValue = NULL;
IADsLargeInteger *pLargeInt = NULL;
long lType, lValue;
BSTR bstr,szString;
VARIANT var,varOS,varDate;
VARIANT *pVar;
CHAR *pszBOOL = NULL;

FILETIME filetime;
SYSTEMTIME systemtime;
IDispatch *pDisp = NULL;
DATE date;

//For Octet Strings
void HUGEP *pArray;
ULONG dwSLBound;
ULONG dwSUBound;

hr = pEntry->get_Values(&var);
if (SUCCEEDED(hr))
{
    //Should be safe array containing variants
    if (var.vt==(VT_VARIANT|VT_ARRAY))
    {
        hr = SafeArrayAccessData((SAFEARRAY*)(var.pparray), (void HUGEP* FAR*)&pVar);
        long lLBound, lUBound;
        //One dimensional array. Get the bounds for the array.
        hr = SafeArrayGetLBound((SAFEARRAY*)(var.pparray), 1, &lLBound);
        hr = SafeArrayGetUBound((SAFEARRAY*)(var.pparray), 1, &lUBound);
        //Get the count of elements
        long cElements = lUBound-lLBound + 1;
        //Get the elements of the array
        if (SUCCEEDED(hr))
        {
            for (int i = 0; i < cElements; i++ )
            {
                switch (pVar[i].vt)
                {
                case VT_BSTR:
                    wprintf(L"%s ",pVar[i].bstrVal);
                    break;
                case VT_DISPATCH:
                    hr = V_DISPATCH(&pVar[i])->QueryInterface(IID_IADsPropertyValue, (void**)&pValue);
                    if (SUCCEEDED(hr))
                    {
                    hr = pValue->get_ADsType(&lType);
                    switch (lType)
                    {
                    case ADSTYPE_DN_STRING:
                        hr = pValue->get_DNString(&bstr);
                        wprintf(L"%s ",bstr);
                        SysFreeString(bstr);
                        break;
                    case ADSTYPE_CASE_IGNORE_STRING:
                        hr = pValue->get_CaseIgnoreString(&bstr);
                        wprintf(L"%s ",bstr);
                        SysFreeString(bstr);
                        break;
                    case ADSTYPE_BOOLEAN:
                        hr = pValue->get_Boolean(&lValue);
                        pszBOOL = lValue ? "TRUE" : "FALSE";
                        wprintf(L"%s ",pszBOOL);
                        break;
                    case ADSTYPE_INTEGER:
                        hr = pValue->get_Integer(&lValue);
                        wprintf(L"%d ",lValue);
                        break;
                    case ADSTYPE_OCTET_STRING:
                        //Get the name of the property to handle
                        //the properties we're interested in.
                        pEntry->get_Name(&szString);
                        hr = pValue->get_OctetString(&varOS);
                        //Get a pointer to the bytes in the octet string.
                        if (SUCCEEDED(hr))
                        {
                            hr = SafeArrayGetLBound( V_ARRAY(&varOS),
                                                        1,
                                                        (long FAR
*) &dwSLBound );
                            hr = SafeArrayGetUBound( V_ARRAY(&varOS),
                                                        1,
                                                        (long FAR
*) &dwSUBound );
                            if (SUCCEEDED(hr))
                            {
                                hr = SafeArrayAccessData( V_ARRAY(&varOS),

&pArray );
                            }
                            if (0==wcscmp(L"objectGUID", szString))
                            {
                                LPOLESTR szDSGUID = new WCHAR [39];
                                //Cast to LPGUID
                                LPGUID pObjectGUID = (LPGUID)pArray;
                                //Convert GUID to string.
                                ::StringFromGUID2(*pObjectGUID, szDSGUID, 39);
                                //Print the GUID
                                wprintf(L"%s ",szDSGUID);
                            }
                            else if (0==wcscmp(L"objectSid", szString))
                            {
                                PSID pObjectSID = (PSID)pArray;
                                //Convert SID to string.
                                LPOLESTR szSID = NULL;
                                ConvertSidToStringSid(pObjectSID, &szSID);
                                wprintf(L"%s ",szSID);
                                LocalFree(szSID);
                            }
                            else
                            {
                                wprintf(L"Value of type Octet String. No
Conversion.");
                            }
                                SafeArrayUnaccessData( V_ARRAY(&varOS) );
                                VariantClear(&varOS);
                        }

                        SysFreeString(szString);

                        break;
                    case ADSTYPE_UTC_TIME:
                        //wprintf(L"Value of type UTC_TIME\n");
                        hr = pValue->get_UTCTime(&date);
                        if (SUCCEEDED(hr))
                        {
                            //Pack in variant.vt
                            varDate.vt = VT_DATE;
                            varDate.date = date;

VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);
                            wprintf(L"%s ",varDate.bstrVal);
                            VariantClear(&varDate);
                        }
                        break;
                    case ADSTYPE_LARGE_INTEGER:
                        //wprintf(L"Value of type Large Integer\n");
                        //Get the name of the property to handle
                        //the properties we're interested in.
                        pEntry->get_Name(&szString);
                        hr = pValue->get_LargeInteger(&pDisp);
                        if (SUCCEEDED(hr))
                        {
                            hr = pDisp->QueryInterface(IID_IADsLargeInteger,
(void**)&pLargeInt);
                            if (SUCCEEDED(hr))
                            {
                                hr =
pLargeInt->get_HighPart((long*)&filetime.dwHighDateTime);
                                hr =
pLargeInt->get_LowPart((long*)&filetime.dwLowDateTime);
                                if((filetime.dwHighDateTime==0) &&
(filetime.dwLowDateTime==0))
                                {
                                    wprintf(L"No Value ");
                                }
                                else
                                {
                                    //Check for properties of type LargeInteger
that represent time
                                    //if TRUE, then convert to variant time.
                                    if ((0==wcscmp(L"accountExpires", szString))|
                                        (0==wcscmp(L"badPasswordTime",
szString))||
                                        (0==wcscmp(L"lastLogon", szString))||
                                        (0==wcscmp(L"lastLogoff",
szString))||
                                        (0==wcscmp(L"lockoutTime",
szString))||
                                        (0==wcscmp(L"pwdLastSet", szString))
                                        )
                                    {
                                        //Handle special case for Never
Expires where low part is -1
                                        if (filetime.dwLowDateTime==-1)
                                        {
                                            wprintf(L"Never Expires ");
                                        }
                                        else
                                        {
                                            if
(FileTimeToLocalFileTime(&filetime, &filetime) != 0)
                                            {
                                                if
(FileTimeToSystemTime(&filetime,

        &systemtime) != 0)
                                                {
                                                    if
(SystemTimeToVariantTime(&systemtime,

                    &date) != 0)
                                                    {

//Pack in variant.vt

varDate.vt = VT_DATE;

varDate.date = date;

VariantChangeType(&varDate,&varDate,VARIANT_NOVALUEPROP,VT_BSTR);

wprintf(L"%s ",varDate.bstrVal);

VariantClear(&varDate);
                                                    }
                                                    else
                                                    {

wprintf(L"FileTimeToVariantTime failed ");
                                                    }
                                                }
                                                else
                                                {

wprintf(L"FileTimeToSystemTime failed ");
                                                }

                                            }
                                            else
                                            {

wprintf(L"FileTimeToLocalFileTime failed ");
                                            }
                                        }
                                    }
                                    //Print the LargeInteger.
                                    else
                                    {
                                        wprintf(L"Large Integer: high: %d
low: %d ",filetime.dwHighDateTime, filetime.dwLowDateTime);
                                    }
                                }
                            }
                            if (pLargeInt)
                                pLargeInt->Release();
                        }
                        else
                        {
                            wprintf(L"Could not get Large Integer");
                        }

                        if (pDisp)
                            pDisp->Release();

                        break;
                    case ADSTYPE_NT_SECURITY_DESCRIPTOR:
                        wprintf(L"Value of type NT Security Descriptor ");
                        break;
                    case ADSTYPE_PROV_SPECIFIC:
                        wprintf(L"Value of type Provider Specific ");
                        break;
                    default:
                        wprintf(L"Unhandled ADSTYPE for property value: %d ",lType);
                        break;
                    }
                    }
                    else
                    {
                        wprintf(L"QueryInterface failed for IADsPropertyValue. HR: %x\n", hr);
                    }
                    if (pValue)
                        pValue->Release();
                    break;
                default:
                    wprintf(L"Unhandled Variant type for property value array: %d\n",pVar[i].vt);
                    break;
                }
            }
            wprintf(L"\n");
        }
        //Decrement the access count for the array.
        SafeArrayUnaccessData((SAFEARRAY*)(var.pparray));

    }
}

    return hr;
}

HRESULT FindUserByName(IDirectorySearch *pSearchBase, //Container to search
                        LPOLESTR szFindUser, //Name of user to find.
                        IADs **ppUser) //Return a pointer to the user
{
    HRESULT hrObj = E_FAIL;
    HRESULT hr = E_FAIL;
    if ((!pSearchBase)||(!szFindUser))
        return E_INVALIDARG;
    //Create search filter
    LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH];
    LPOLESTR szADsPath = new OLECHAR[MAX_PATH];
    wcscpy(pszSearchFilter, L"(&(objectCategory=person)(objectClass=user)(cn=");
    wcscat(pszSearchFilter, szFindUser);
    wcscat(pszSearchFilter,    L"))");
        //Search entire subtree from root.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_SUBTREE;
        DWORD dwNumPrefs = 1;
    // COL for iterations
        ADS_SEARCH_COLUMN col;
        // Handle used for searching
        ADS_SEARCH_HANDLE hSearch;
    // Set the search preference
        hr = pSearchBase->SetSearchPreference( &SearchPrefs, dwNumPrefs);
        if (FAILED(hr))
            return hr;
    // Set attributes to return
    CONST DWORD dwAttrNameSize = 1;
        LPOLESTR pszAttribute[dwAttrNameSize] = {L"ADsPath"};

        // Execute the search
        hr = pSearchBase->ExecuteSearch(pszSearchFilter,
                                pszAttribute,
                                    dwAttrNameSize,
                                    &hSearch
                                );
    if (SUCCEEDED(hr))
    {

    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pSearchBase->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {
            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {
                // Get the data for this column
                hr = pSearchBase->GetColumn( hSearch, pszAttribute[x], &col );
                if ( SUCCEEDED(hr) )
                {
                    // Print the data for the column and free the column
                    // Note the requested attribute is type CaseIgnoreString.
                    wcscpy(szADsPath, col.pADsValues->CaseIgnoreString);
                    hr = ADsOpenObject(szADsPath,
                                    NULL,
                                    NULL,
                                    ADS_SECURE_AUTHENTICATION, //Use Secure
Authentication
                                    IID_IADs,
                                    (void**)ppUser);
                    if (SUCCEEDED(hr))
                    {
                       wprintf(L"Found User.\n",szFindUser);
                       wprintf(L"%s: %s\r\n",pszAttribute[x],col.pADsValues->CaseIgnoreString);
                        hrObj = S_OK;
                    }
                    pSearchBase->FreeColumn( &col );
                }
                else
                    hr = E_FAIL;
            }
        }
        // Close the search handle to clean up
        pSearchBase->CloseSearchHandle(hSearch);
    }
    if (FAILED(hrObj))
        hr = hrObj;
    return hr;
}
[Visual Basic]
Dim sUserName As String
    Dim sSearchFilter As String
    Dim lScope As Integer
    Dim iIndex As Integer
    iIndex = 0
    Dim v, j, i
    Dim ds As IADs
    Dim con As New Connection, rs As New Recordset
    Dim Com As New Command
    Dim oIADs As IADs
    Dim sLdap As String
    Dim sUserADsPath As String

    sUserName = InputBox("This script Enumerates the properties of a user on a domain." & vbCrLf & vbCrLf & "Specify the name of the user:")

    If sUserName = "" Then
        Exit Sub
    End If

    ' Bind to the rootDSE
    Set ds = GetObject("LDAP://RootDSE")
    sLdap = "LDAP://" & ds.Get("defaultNamingContext")
    Set ds = Nothing

    Set oIADs = GetObject(sLdap)

    sSearchFilter = "CN='" & sUserName & "'"

    'Open a Connection object
    con.Provider = "ADsDSOObject"

    '---------------------------------------------------------------------
    ' If you want to be authenticated other than current logged on user
    ' use connection properties of User ID and Password
    '---------------------------------------------------------------------
    ' con.Properties("User ID") = "Administrator"
    ' con.Properties("Password") = ""

    ' Open the connection
    con.Open "Active Directory Provider"

    ' Create a command object on this connection
    Set Com.ActiveConnection = con

    ' set the query string using SQL Dialect
    Com.CommandText = "select name,AdsPath from '" & oIADs.ADsPath & "' where " & sSearchFilter & " ORDER BY NAME"

    ' Tell the user what the search filter is
    'MsgBox "Search Filter = " & Com.CommandText

    '---------------------------------------------------
    ' Or you can use LDAP Dialect, for example,
    '---------------------------------------------------
    ' Ex Com.CommandText = "<LDAP://ntdsdc1/dc=NTDEV,DC=microsof,DC=com>;(objectClass=*);name"
    ' For LDAP Dialect, the valid search scope are base, oneLevel and subtree
    ' Com.CommandText = "<" & adDomainPath & ">;(objectClass=*);name;subtree"
    ' For LDAP Dialect (<LDAP:...>), there is no way to specify sort order in the string,
    ' However, you can use this SORT ON property to specify sort order.
    ' for SQL Dialect you can use ORDER BY in the SQL Statement
    ' Ex. Com.Properties("Sort On") = "Name"

    'Set the preferences for Search
    Com.Properties("Page Size") = 1000
    Com.Properties("Timeout") = 30 'seconds
    Com.Properties("searchscope") = ADS_SCOPE_SUBTREE
    Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
    Com.Properties("Cache Results") = False ' do not cache the result, it results in less memory requirements
    Com.Properties("Size Limit") = 1 ' Limit to 1 Result

    'Execute the query
    Set rs = Com.Execute

    ' Navigate the record set
     If Not rs.EOF Then
        rs.MoveFirst
    End If

    On Error Resume Next
    If Not rs.EOF Then
        ' Display the LDAP path for the row
        MsgBox "Found the user " & sUserName & " at " & rs.Fields("AdsPath")
        sUserADsPath = rs.Fields("AdsPath")
        rs.MoveNext
    Else
        MsgBox "Did not find the username " & sUserName & " in the directory"
        Exit Sub
    End If

    Set ds = Nothing
    Set con = Nothing
    Set rs = Nothing
    Set Com = Nothing
    Set oIADs = Nothing

    ' Now enumerate the properties
    Dim propList As IADsPropertyList
    Dim propEnty As IADsPropertyEntry
    Dim propVal As IADsPropertyValue
    Dim count As Long

    Dim sOutput As String
    Dim currentcount As Long

    Const NumToDisplayAtAtime As Integer = 10

    ' Bind to the user
    Set propList = GetObject(sUserADsPath)

    ' Bring the properties into the cache
    propList.GetInfo

    count = propList.PropertyCount
    sOutput = "No of Property Found: " & Str(count) & vbCrLf & vbCrLf

    For i = 0 To count - 1
        currentcount = currentcount + 1
        'Each item in property list has a property entry
        Set propEntry = propList.Item(i)

        ' Append to outputstring
        sOutput = sOutput & "PROPERTYENTRY NAME:" & propEntry.Name & vbCrLf & " ------" & vbCrLf

        'Each value in property entry has property values
        For Each v In propEntry.Values
            Set propVal = v
            ' Append to outputstring
            sOutput = sOutput & propVal.CaseIgnoreString vbCrLf
        Next
        If currentcount = NumToDisplayAtAtime Then
            MsgBox sOutput
            sOutput = ""
            currentcount = 0
        End If
    Next
    Set propList = Nothing
    Set propEnty = Nothing
    Set propVal = Nothing


Example Code for List Non-Replicated, Global Catalog, and Constructed Properties

The following C/C++ program queries for four types of properties that are 1) non-replicated, 2) indexed, 3) global catalog, and 4) constructed:

// Displays attributes of different types.
//
#include "stdafx.h"
#include <wchar.h>
#include <activeds.h>
#define ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED         (0x00000001) // Attribute is not replicated
#define ADS_SYSTEMFLAG_ATTR_REQ_PARTIAL_SET_MEMBER (0x00000002) // Attribute is required to be
                                                      //   member of the partial set
#define ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED         (0x00000004) // Attribute is a constructed att

HRESULT FindAttributesByType(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        DWORD dwAttributeType, //Bit flags to search for in systemFlags
        LPOLESTR pszAttributerNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bIsExactMatch //TRUE to find attributes that have systemFlags exactly matching dwAttributeType
                            //FALSE to find attributes that have the dwAttributeType bit set (and possibly others).
        );

HRESULT FindIndexedAttributes(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        LPOLESTR pszAttributeNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bIsIndexed //TRUE to find indexed attributes.
                        //FALSE to find non-indexed attributes.
        );

HRESULT FindGCAttributes(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        LPOLESTR pszAttributeNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bInGC //TRUE to find indexed attributes.
                         //FALSE to find non-indexed attributes.
        );

//#define ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED         (0x00000001) // Non-replicated attribute
//#define ADS_SYSTEMFLAG_ATTR_REQ_PARTIAL_SET_MEMBER (0x00000002) // Attribute is required to be
                                                      //   member of the partial set
//#define ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED         (0x00000004) // Attribute is a constructed att

int main(int argc, char* argv[])
{
HRESULT hr = E_FAIL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
IDirectorySearch *pSchemaNC = NULL;
IADs *pObject = NULL;
VARIANT var;

//Intialize COM
CoInitialize(NULL);

//Get rootDSE and the schema container's distinguished name.
//Bind to current user's domain using current user's security context.
hr = ADsOpenObject(L"LDAP://rootDSE",
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IADs,
                (void**)&pObject);
if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"schemaNamingContext",&var);
    if (SUCCEEDED(hr))
    {
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    hr = ADsOpenObject(szPath,
                    NULL,
                    NULL,
                    ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                    IID_IDirectorySearch,
                    (void**)&pSchemaNC);
    if (SUCCEEDED(hr))
    {
    //Find non-replicated attributes
    wprintf(L"Find non-replicated attributes\n");
    hr = FindAttributesByType(pSchemaNC, //IDirectorySearch pointer to schema naming context.
        ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED, //Bit flags to search for in systemFlags
        NULL,
        TRUE
        );

    //Find attributes included in the global catalog
    wprintf(L"Find attributes included in the global catalog\n");
    hr = FindGCAttributes(pSchemaNC,
        NULL,
        TRUE
        );

    //Find constructed attributes
    wprintf(L"Find constructed attributes\n");
    hr = FindAttributesByType(pSchemaNC, //IDirectorySearch pointer to schema naming context.
        ADS_SYSTEMFLAG_ATTR_IS_CONSTRUCTED, //Bit flags to search for in systemFlags
        NULL,
        FALSE
        );

    //Find indexed attributes
    wprintf(L"Find indexed attributes\n");
    hr = FindIndexedAttributes(pSchemaNC, //IDirectorySearch pointer to schema naming context.
        NULL,
        TRUE
        );
    }
    if (pSchemaNC)
        pSchemaNC->Release();
    }
}
if (pObject)
    pObject->Release();
VariantClear(&var);

// Uninitialize COM
CoUninitialize();
return 0;
}

HRESULT FindAttributesByType(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        DWORD dwAttributeType, //Bit flags to search for in systemFlags
        LPOLESTR pszAttributeNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bIsExactMatch //TRUE to find attributes that have systemFlags exactly matching dwAttributeType
                            //FALSE to find attributes that have the dwAttributeType bit set (and possibly others).
        )
{
    //Create search filter
    LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];
    if (bIsExactMatch)
        //Find attributes with systemFlags that exactly match dwAttributeType
        wsprintf(pszSearchFilter, L"(&(objectCategory=attributeSchema)(systemFlags=%d))",dwAttributeType);
    else
        //Find attributes with systemFlags that contain dwAttributeType
        wsprintf(pszSearchFilter, L"(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804:=%d))",dwAttributeType);

    //Attributes are one-level deep in the Schema container so only need to search one level.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
    DWORD dwNumPrefs = 1;

    // COL for iterations
        ADS_SEARCH_COLUMN col;
        HRESULT hr;

        // Interface Pointers
        IADs    *pObj = NULL;
        IADs    * pIADs = NULL;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;

    // Set the search preference
        hr = pSchemaNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);
        if (FAILED(hr))
            return hr;

    CONST DWORD dwAttrNameSize = 1;
        LPOLESTR pszAttribute[dwAttrNameSize];
        if (!pszAttributeNameType)
            pszAttribute[0] = L"cn";
        else
            pszAttribute[0] = pszAttributeNameType;
    // Execute the search
    hr = pSchemaNC->ExecuteSearch(pszSearchFilter,
                                  pszAttribute,
                                    dwAttrNameSize,
                                    &hSearch
                                    );
    if ( SUCCEEDED(hr) )
    {
    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pSchemaNC->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {
            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {
                // Get the data for this column
                hr = pSchemaNC->GetColumn( hSearch, pszAttribute[x], &col );
                if ( SUCCEEDED(hr) )
                {
                    // Print the data for the column and free the column
                    wprintf(L"%s: %s\r\n",pszAttribute[x],col.pADsValues->CaseIgnoreString);
                    pSchemaNC->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property is not a string>",pszAttribute[x]);
            }
        }
        // Close the search handle to clean up
        pSchemaNC->CloseSearchHandle(hSearch);
    }
    return hr;
}

HRESULT FindIndexedAttributes(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        LPOLESTR pszAttributeNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bIsIndexed //TRUE to find indexed attributes.
                         //FALSE to find non-indexed attributes.
        )
{
    //Create search filter
    LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];
    DWORD dwIndexed;
    if (bIsIndexed)
        dwIndexed = 1;
    else
         dwIndexed = 0;

    wsprintf(pszSearchFilter, L"(&(objectCategory=attributeSchema)(searchFlags=%d))",dwIndexed);

    //Attributes are one-level deep in the Schema container so only need to search one level.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
    DWORD dwNumPrefs = 1;

    // COL for iterations
    ADS_SEARCH_COLUMN col;
    HRESULT hr;

    // Interface Pointers
    IADs    *pObj = NULL;
    IADs    * pIADs = NULL;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;

    // Set the search preference
    hr = pSchemaNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);
    if (FAILED(hr))
        return hr;

    CONST DWORD dwAttrNameSize = 1;
    LPOLESTR pszAttribute[dwAttrNameSize];

    if (!pszAttributeNameType)
        pszAttribute[0] = L"cn";
    else
        pszAttribute[0] = pszAttributeNameType;

    // Execute the search
    hr = pSchemaNC->ExecuteSearch(pszSearchFilter,
                                  pszAttribute,
                                    dwAttrNameSize,
                                    &hSearch
                                    );
    if ( SUCCEEDED(hr) )
    {
    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pSchemaNC->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {
            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {
                // Get the data for this column
                hr = pSchemaNC->GetColumn( hSearch, pszAttribute[x], &col );
                if ( SUCCEEDED(hr) )
                {
                    // Print the data for the column and free the column
                    wprintf(L"%s: %s\r\n",pszAttribute[x],col.pADsValues->CaseIgnoreString);
                    pSchemaNC->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property is not a string>",pszAttribute[x]);
            }
        }
        // Close the search handle to clean up
        pSchemaNC->CloseSearchHandle(hSearch);
    }
    return hr;
}

HRESULT FindGCAttributes(IDirectorySearch *pSchemaNC, //IDirectorySearch pointer to schema naming context.
        LPOLESTR pszAttributeNameType, //ldapDisplayName of the naming attribute you want to display for each returned attribute.
                                         //NULL returns common name.
        BOOL bInGC //TRUE to find GC attributes.
                         //FALSE to find non-GC attributes.
        )
{
    //Create search filter
    LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];
    LPOLESTR szBool = NULL;
    if (bInGC)
        szBool = L"TRUE";
    else
        szBool = L"FALSE";

    wsprintf(pszSearchFilter, L"(&(objectCategory=attributeSchema)(isMemberOfPartialAttributeSet=%s))",szBool);
    //Attributes are one-level deep in the Schema container so only need to search one level.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
    DWORD dwNumPrefs = 1;
    // COL for iterations
    ADS_SEARCH_COLUMN col;
    HRESULT hr;
    // Interface Pointers
    IADs    *pObj = NULL;
    IADs    * pIADs = NULL;
    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;
    // Set the search preference
    hr = pSchemaNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);
    if (FAILED(hr))
        return hr;

    CONST DWORD dwAttrNameSize = 1;
    LPOLESTR pszAttribute[dwAttrNameSize];

    if (!pszAttributeNameType)
        pszAttribute[0] = L"cn";
    else
        pszAttribute[0] = pszAttributeNameType;

    // Execute the search
    hr = pSchemaNC->ExecuteSearch(pszSearchFilter,
                                  pszAttribute,
                                  dwAttrNameSize,
                                  &hSearch
                                  );
    if ( SUCCEEDED(hr) )
    {
    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pSchemaNC->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {
            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {
                // Get the data for this column
                hr = pSchemaNC->GetColumn( hSearch, pszAttribute[x], &col );
                if ( SUCCEEDED(hr) )
                {
                    // Print the data for the column and free the column
                    wprintf(L"%s: %s\r\n",pszAttribute[x],col.pADsValues->CaseIgnoreString);
                    pSchemaNC->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property is not a string>",pszAttribute[x]);
            }
        }

        // Close the search handle to clean up
        pSchemaNC->CloseSearchHandle(hSearch);
    }
    return hr;
}


Example Code for Reading a Constructed Property

The following code fragments read the canonicalName property of the Administrator.

[C++]
//Read the canonicalName of Administrator
#include <wchar.h>
#include <objbase.h>
#include <activeds.h>

void wmain( int argc, wchar_t *argv[ ])
{

//Intialize COM
CoInitialize(NULL);
HRESULT hr = S_OK;
//Get rootDSE and the domain container's DN.
IADs *pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;
hr = ADsOpenObject(L"LDAP://rootDSE",
                 NULL,
                 NULL,
                 ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                 IID_IADs,
                 (void**)&pObject);
if (FAILED(hr))
{
   wprintf(L"Not Found. Could not bind to the domain.\n");
   if (pObject)
     pObject->Release();
   return;
}

hr = pObject->Get(L"defaultNamingContext",&var);
if (SUCCEEDED(hr))
{
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,L"cn=Administrator,cn=Users,");
    wcscat(szPath,var.bstrVal);
    VariantClear(&var);
    if (pObject)
    {
       pObject->Release();
       pObject = NULL;
    }
    //Bind to the Administrator user object.
    hr = ADsOpenObject(szPath,
                     NULL,
                     NULL,
                     ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                     IID_IADs,
                     (void**)&pObject);
    if (SUCCEEDED(hr))
    {
        //
        LPOLESTR pwszArray[] = {L"canonicalName"};
        DWORD dwArrayItems = sizeof(pwszArray)/sizeof(LPOLESTR);
        VARIANT vArray;
        VariantInit(&vArray);
        // Build a Variant of array type, using the specified string array.
        hr = ADsBuildVarArrayStr(pwszArray, dwArrayItems, &vArray);
        if (SUCCEEDED(hr))
        {
            hr = pObject->GetInfoEx(vArray,0L);
            hr = pObject->Get(L"canonicalName", &var);
            if (SUCCEEDED(hr))
                wprintf(L"canonicalName: %s\n",var.bstrVal);
            else
                wprintf(L"Get failed with hr: %x\n",hr);
            VariantClear(&var);
        }
    }
    if (pObject)
        pObject->Release();
}

//Uninitialize COM
CoUninitialize();

    return;
}
[Visual Basic]
'Read the canonicalName of Administrator
Dim rootDSE As IADs
Dim user As IADs

sPrefix = "LDAP://"
Set rootDSE = GetObject(sPrefix & "rootDSE")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If
sDomain = rootDSE.Get("defaultNamingContext")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get method"
End If
sUserDN = "cn=Administrator,cn=users," + sDomain
'''''''''''''''''''''''''''''''''''''''
'Bind to the Administrator user
'''''''''''''''''''''''''''''''''''''''
Set user = GetObject(sPrefix & sUserDN)
user.GetInfoEx Array("canonicalName"), 0
strText = "Canonical Name: " & user.Get("canonicalName")
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Get method"
End If
show_items strText, sComputer

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Get canonicalName for Administrator"
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Setting Properties on a User Object

There are two ways to set properties on a user object (or any object):

  • IDirectoryObject::SetObjectAttributes method. You can set properties at the server directly (no local caching).
  • Property cache methods (IADs and IADsPropertyList). You can set the property values in the property cache as necessary for your application using IADs methods or IADsPropertyList methods, then write the changes back out to the directory service when they are in a consistent state using SetInfo.

Regardless of the way you set properties, you need to know at least two pieces of information about the property: 1) What is the property's syntax (data type)? and 2) Does the property allow multiple values?

For a table listing all default properties for a user with their data types and whether they allow single or multiple values, see Data Types for User Object Properties.

For a sample application that sets properties, see the CreateUser sample, which creates a user from properties specified in an initialization file.

Setting Passwords

[This is preliminary documentation and subject to change.]

For the password of the user, you should use the IADsUser::ChangePassword method (if your script or application is allowing the user to change his/her own password) or IADsUser::SetPassword method (if your script or application is allowing administrator reset a password).


Example Code for Setting User Passwords

The following Visual Basic® code snippet shows how to set the user password, assuming the logged on user has the permission to set the password.

Set usr = GetObject("LDAP://cn=foobar,cn=Users,dc=ArcadiaBay,dc=com")
usr.SetPassword "topsecret98"


Data Types for User Object Properties

The following table contains all base properties for the user object (that is, properties defined by default in the user classSchema object) with their syntax and whether they can use single or multiple values:

Property (ldapDisplayName) | Syntax (Data Type) | Single\Multiple Values
accountExpires | INTEGER8 | Single
aCSPolicyName | DirectoryString | Single
adminCount | INTEGER | Single
adminDescription | DirectoryString | Single
adminDisplayName | DirectoryString | Single
allowedAttributes | OID | Multiple
allowedAttributesEffective | OID | Multiple
allowedChildClasses | OID | Multiple
allowedChildClassesEffective | OID | Multiple
alternateSecurityIdentities | IA5String | Multiple
altRecipient | DN | Single
altRecipientBL | DN | Multiple
altSecurityIdentities | DirectoryString | Multiple
ancestorID | OctetString | Single
assistant | DN | Single
assocRemoteDXA | DN | Multiple
attributeCertificate | OctetString | Multiple
AuthOrig | ORName | Multiple
AuthOrigBL | DN | Multiple
autoReply | Boolean | Single
autoReplyMessage | DirectoryString | Single
autoReplySubject | DirectoryString | Single
badPasswordTime | INTEGER8 | Single
badPwdCount | INTEGER | Single
bridgeheadServerListBL | DN | Multiple
c | DirectoryString | Single
CanCreatePFBL | DN | Multiple
CanCreatePFDLBL | DN | Multiple
CanNotCreatePFBL | DN | Multiple
CanNotCreatePFDLBL | DN | Multiple
canonicalName | DirectoryString | Multiple
cn | DirectoryString | Single
co | DirectoryString | Single
codePage | INTEGER | Single
comment | DirectoryString | Single
controlAccessRights | OctetString | Multiple
countryCode | INTEGER | Single
createTimeStamp | GeneralizedTime | Single
dBCSPwd | OctetString | Single
defaultClassStore | DN | Multiple
deletedItemFlags | INTEGER | Single
delivContLength | INTEGER | Single
delivEITs | OctetString | Multiple
deliverAndRedirect | Boolean | Single
delivExtContTypes | OctetString | Multiple
department | DirectoryString | Single
description | DirectoryString | Multiple
desktopProfile | DirectoryString | Single
destinationIndicator | PrintableString | Multiple
directReports | DN | Multiple
displayName | DirectoryString | Single
displayNamePrintable | PrintableString | Single
distinguishedName | DN | Single
division | DirectoryString | Single
DLMemRejectPerms | ORName | Multiple
DLMemRejectPermsBL | DN | Multiple
DLMemSubmitPerms | ORName | Multiple
DLMemSubmitPermsBL | DN | Multiple
dnQualifier | DirectoryString | Single
dSASignature | OctetString | Single
dynamicLDAPServer | DN | Single
employeeID | DirectoryString | Single
expirationTime | UTCTime | Single
extensionAttribute1 | DirectoryString | Single
extensionAttribute10 | DirectoryString | Single
extensionAttribute11 | DirectoryString | Single
extensionAttribute12 | DirectoryString | Single
extensionAttribute13 | DirectoryString | Single
extensionAttribute14 | DirectoryString | Single
extensionAttribute15 | DirectoryString | Single
extensionAttribute2 | DirectoryString | Single
extensionAttribute3 | DirectoryString | Single
extensionAttribute4 | DirectoryString | Single
extensionAttribute5 | DirectoryString | Single
extensionAttribute6 | DirectoryString | Single
extensionAttribute7 | DirectoryString | Single
extensionAttribute8 | DirectoryString | Single
extensionAttribute9 | DirectoryString | Single
extensionData | OctetString | Multiple
extensionName | DirectoryString | Multiple
extensionNameInherited | DirectoryString | Multiple
facsimileTelephoneNumber | DirectoryString | Single
flags | INTEGER | Single
forwardingAddress | DirectoryString | Single
fromEntry | Boolean | Multiple
frsComputerReferenceBL | DN | Multiple
fRSMemberReferenceBL | DN | Multiple
fSMORoleOwner | DN | Single
garbageCollPeriod | INTEGER | Single
gatewayProxy | DirectoryString | Multiple
generationQualifier | DirectoryString | Single
givenName | DirectoryString | Single
gPLink | DirectoryString | Single
gPOptions | INTEGER | Single
groupMembershipSAM | OctetString | Single
groupPriority | DirectoryString | Multiple
groupsToIgnore | DirectoryString | Multiple
heuristics | INTEGER | Single
homeDirectory | DirectoryString | Single
homeDrive | DirectoryString | Single
homeMDB | DN | Single
homeMDBBL | DN | Multiple
homeMTA | DN | Single
homePhone | DirectoryString | Single
homePostalAddress | DirectoryString | Single
iMOBuiltinAccountOpsGroup | DN | Multiple
iMOBuiltinAdminGroup | DN | Multiple
iMOBuiltinBackupGroup | DN | Multiple
iMOBuiltinGuestGroup | DN | Multiple
iMOBuiltinPrintOpsGroup | DN | Multiple
iMOBuiltinReplicatorGroup | DN | Multiple
iMOBuiltinServerOpsGroup | DN | Multiple
iMOBuiltinUsersGroup | DN | Multiple
importedFrom | DirectoryString | Single
incomingMsgSizeLimit | INTEGER | Single
initials | DirectoryString | Single
instanceType | INTEGER | Single
internationalISDNNumber | NumericString | Multiple
internetEncoding | INTEGER | Single
ipPhone | DirectoryString | Single
isCriticalSystemObject | Boolean | Single
isDeleted | Boolean | Single
isPrivilegeHolder | DN | Multiple
l | DirectoryString | Single
LabeledURI | DirectoryString | Multiple
language | DirectoryString | Single
languageCode | INTEGER | Single
lastKnownParent | DN | Single
lastLogoff | INTEGER8 | Single
lastLogon | INTEGER8 | Single
legacyExchangeDN | CaseIgnoreString | Single
lmPwdHistory | OctetString | Multiple
localeID | INTEGER | Multiple
lockoutTime | INTEGER8 | Single
logonCount | INTEGER | Single
logonHours | OctetString | Single
logonWorkstation | OctetString | Single
mail | DirectoryString | Single
mailDrop | DirectoryString | Single
mailNickname | DirectoryString | Single
maintainAutoReplyHistory | Boolean | Single
managedObjects | DN | Multiple
manager | DN | Single
mAPIRecipient | Boolean | Single
maxStorage | INTEGER8 | Single
memberOf | DN | Multiple
mhsORAddress | DirectoryString | Multiple
middleName | DirectoryString | Single
mobile | DirectoryString | Single
modifyTimeStamp | GeneralizedTime | Single
mSMQDigests | OctetString | Multiple
mSMQDigestsMig | OctetString | Multiple
mSMQSignCertificates | OctetString | Single
mSMQSignCertificatesMig | OctetString | Single
msNPAllowDialin | Boolean | Single
msNPCallingStationID | IA5String | Multiple
msNPSavedCallingStationID | IA5String | Multiple
msRADIUSCallbackNumber | IA5String | Single
msRADIUSFramedIPAddress | INTEGER | Single
msRADIUSFramedRoute | IA5String | Multiple
msRADIUSServiceType | INTEGER | Single
msRASSavedCallbackNumber | IA5String | Single
msRASSavedFramedIPAddress | INTEGER | Single
msRASSavedFramedRoute | IA5String | Multiple
name | DirectoryString | Single
netbootSCPBL | DN | Single
networkAddress | CaseIgnoreString | Multiple
nNTPCharacterSet | DirectoryString | Single
nNTPContentFormat | DirectoryString | Single
nNTPNewsfeeds | DN | Multiple
nonSecurityMemberBL | DN | Multiple
notes | DirectoryString | Single
ntPwdHistory | OctetString | Multiple
nTSecurityDescriptor | ObjectSecurity Descriptor | Single
o | DirectoryString | Multiple
objectCategory | DN | Single
objectClass | OID | Multiple
objectGUID | OctetString | Single
objectSid | OctetString | Single
objectVersion | INTEGER | Single
objViewContainers | DN | Multiple
operatorCount | INTEGER | Single
otherFacsimileTelephoneNumber | DirectoryString | Multiple
otherHomePhone | DirectoryString | Multiple
otherIpPhone | DirectoryString | Multiple
otherLoginWorkstations | DirectoryString | Multiple
otherMailbox | DirectoryString | Multiple
otherMobile | DirectoryString | Multiple
otherPager | DirectoryString | Multiple
otherTelephone | DirectoryString | Multiple
ou | DirectoryString | Multiple
overrideNNTPContentFormat | Boolean | Single
ownerBL | DN | Single
pager | DirectoryString | Single
partialAttributeDeletionList | OctetString | Single
partialAttributeSet | OctetString | Single
periodReplStagger | INTEGER | Single
periodRepSyncTimes | OctetString | Single
personalTitle | DirectoryString | Single
physicalDeliveryOfficeName | DirectoryString | Single
pOPCharacterSet | DirectoryString | Single
pOPContentFormat | DirectoryString | Single
possibleInferiors | OID | Multiple
postalAddress | DirectoryString | Multiple
postalCode | DirectoryString | Single
postOfficeBox | DirectoryString | Multiple
preferredDeliveryMethod | INTEGER | Multiple
preferredOU | DN | Single
primaryGroupID | INTEGER | Single
primaryInternationalISDNNumber | DirectoryString | Single
primaryTelexNumber | DirectoryString | Single
profilePath | DirectoryString | Single
protocolSettings | DirectoryString | Multiple
proxiedObjectName | DN | Single
proxyAddresses | DirectoryString | Multiple
publicDelegatesBL | DN | Multiple
pwdLastSet | INTEGER8 | Single
queryPolicyBL | DN | Multiple
registeredAddress | OctetString | Multiple
replicatedObjectVersion | INTEGER | Single
replicationSensitivity | INTEGER | Single
replPropertyMetaData | OctetString | Single
replUpToDateVector | OctetString | Single
repsFrom | OctetString | Multiple
repsTo | OctetString | Multiple
repsToExt | OctetString | Multiple
reverseMembership | OctetString | Multiple
revision | INTEGER | Single
rid | INTEGER | Single
sAMAccountName | DirectoryString | Single
sAMAccountType | INTEGER | Single
scriptPath | DirectoryString | Single
securityIdentifier | OctetString | Single
securityProtocol | OctetString | Multiple
seeAlso | DN | Multiple
serverReferenceBL | DN | Single
servicePrincipalName | DirectoryString | Multiple
showInAddressBook | DN | Multiple
showInAdvancedViewOnly | Boolean | Single
sIDHistory | OctetString | Multiple
siteObjectBL | DN | Multiple
sn | DirectoryString | Single
st | DirectoryString | Single
street | DirectoryString | Single
streetAddress | DirectoryString | Single
submissionContLength | INTEGER | Single
subRefs | DN | Multiple
subSchemaSubEntry | DN | Multiple
supplementalCredentials | OctetString | Multiple
supportedAlgorithms | OctetString | Single
supportingStackBL | DN | Multiple
systemFlags | INTEGER | Single
taggedCertificate | OctetString | Multiple
telephoneNumber | DirectoryString | Single
teletexTerminalIdentifier | OctetString | Multiple
telexNumber | OctetString | Multiple
terminalServer | OctetString | Single
textEncodedORAddress | DirectoryString | Single
thumbnailLogo | OctetString | Single
thumbnailPhoto | OctetString | Single
title | DirectoryString | Single
UnauthOrig | ORName | Multiple
UnauthOrigBL | DN | Multiple
unicodePwd | OctetString | Single
url | DirectoryString | Multiple
userAccountControl | INTEGER | Single
userCert | OctetString | Single
userCertificate | OctetString | Multiple
userParameters | DirectoryString | Single
userPassword | OctetString | Multiple
userPrincipalName | DirectoryString | Single
userSharedFolder | DirectoryString | Single
userSharedFolderOther | DirectoryString | Multiple
userSMIMECertificate | OctetString | Multiple
userWorkstations | DirectoryString | Single
useServerValues | Boolean | Single
uSNChanged | INTEGER8 | Single
uSNCreated | INTEGER8 | Single
uSNDSALastObjRemoved | INTEGER8 | Single
USNIntersite | INTEGER | Single
uSNLastObjRem | INTEGER8 | Single
uSNSource | INTEGER8 | Single
viewContainer1 | DirectoryString | Single
viewContainer2 | DirectoryString | Single
viewContainer3 | DirectoryString | Single
viewSite | DirectoryString | Single
voiceMailFlags | OctetString | Multiple
voiceMailGreetings | DirectoryString | Multiple
voiceMailPassword | DirectoryString | Single
voiceMailRecordedName | OctetString | Single
voiceMailRecordingLength | INTEGER | Multiple
voiceMailSpeed | INTEGER | Single
voiceMailSystemGUID | OctetString | Single
voiceMailUserID | DirectoryString | Single
voiceMailVolume | INTEGER | Single
wbemPath | DirectoryString | Multiple
wellKnownObjects | 1.2.840.113556.1.4.903 | Multiple
whenChanged | GeneralizedTime | Single
whenCreated | GeneralizedTime | Single
wWWHomePage | DirectoryString | Single
x121Address | NumericString | Multiple
x500AccessControlList | OctetString | Single


Creating a User

To add a user to Active Directory™, you create a user object in the domain container of the domain where you want to place the user. Users can be created at the root of the domain, within an organizational unit, or within a container.

When you create a user object, you must also set the following attributes to make the object a legal user that is recognized by Active Directory and the Windows® Security system:

cn
Required. Specify the name of the user object in the directory. This will be the object's relative distinguished name (RDN) within the container where you create the user.
sAMAccountName
Required. Specify a string that is the name used to support clients and servers from a previous version of Windows®. Note that the sAMAccountName should be less than 20 characters to support clients from a previous version of Windows.

The sAMAccountName must be unique among all security principal objects within the domain. You should perform a query against the domain to verify that the sAMAccountName is unique within the domain.

Optionally, you can also set other properties. Note that the following user properties are set with default values if you do not explicitly set them at creation time:

Property | Value
accountExpires | Default is Never.
cn | Specified in IADsContainer::Create.
nTSecurityDescriptor | A security descriptor is created based on the rules specified in How Security Descriptors are Set on new Objects.
objectCategory | Person
name | RDN is the cn.
pwdLastSet | Controls whether user must change password at next logon. Default is 0. Zero(0) means the user must change the password at next logon.
UserAccountControl | Contains values that determine several logon and account features for the user. By default, the following flags are set: UF_ACCOUNTDISABLE UF_PASSWD_NOTREQD UF_NORMAL_ACCOUNT UF_ACCOUNTDISABLE means the user's account is disabled. UF_PASSWD_NOTREQD means no password is required for the user to log on. UF_NORMAL_ACCOUNT is a default account type that represents a typical user.
memberOf | Domain Users


Example Code for Creating a User

[C++]

The following code snippet contains a function that creates a user with only the essential properties explicitly set (cn, sAMAccountType) and returns a IDirectoryObject pointer to the new user object:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  CreateUser()   - Function for creating a basic User

    Parameters

        IDirectoryObject *pDirObject    -   Parent Directory Object for the new User
        LPWSTR pwCommonName             -   Common Name for the new User
        IDirectoryObject ** ppDirObjRet -   Pointer to the Pointer which will receive the new User
*/
HRESULT CreateUser(IDirectoryObject *pDirObject, LPWSTR pwCommonName,LPWSTR pwSamAcctName,IDirectoryObject ** ppDirObjRet)
{
    assert(pDirObject);
    if (wcslen(pwSamAcctName) >20)
    {
        MessageBox(NULL,L"SamAccountName CANNOT be bigger than 20 characters",L"Error: CreateSimpleUser()",MB_ICONSTOP);
        assert(0);
        return E_FAIL;
    }

    HRESULT    hr;
    ADSVALUE   sAMValue;
    ADSVALUE   classValue;
    LPDISPATCH pDisp;
    WCHAR       pwCommonNameFull[1024];

    ADS_ATTR_INFO  attrInfo[] =
    {
       { L"objectClass", ADS_ATTR_UPDATE,
                           ADSTYPE_CASE_IGNORE_STRING, &classValue, 1 },
       {L"sAMAccountName", ADS_ATTR_UPDATE,
                           ADSTYPE_CASE_IGNORE_STRING, &sAMValue, 1},
    };

    DWORD dwAttrs = sizeof(attrInfo)/sizeof(ADS_ATTR_INFO);
    classValue.dwType = ADSTYPE_CASE_IGNORE_STRING;
    classValue.CaseIgnoreString = L"User";

    sAMValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
    sAMValue.CaseIgnoreString = pwSamAcctName;

    wsprintfW(pwCommonNameFull,L"CN=%s",pwCommonName);

    hr = pDirObject->CreateDSObject( pwCommonNameFull,  attrInfo,
                                dwAttrs, &pDisp );
    if (SUCCEEDED(hr))
    {
        hr = pDisp->QueryInterface(IID_IDirectoryObject,(void**) ppDirObjRet);

        pDisp->Release();
        pDisp = NULL;
    }
    return hr;
}
[Visual Basic]

The following code snippet creates a user with only the essential properties explicitly set (cn, sAMAccountType) and displays the properties of the new user:

Dim IADsRootDSE As IADs

sComputer = InputBox("This creates a user in a Windows 2000 domain." & vbCrLf & vbCrLf & "Specify the domain name or the name of a domain controller in the domain. (i.e. MyDomain.development.microsoft.com ):")
sContainer = InputBox("Specify the name of the container where you want to create the user : (i.e. CN=Users,DC=MyDomain,DC=Development,DC=microsoft,DC=com")
sUser = InputBox("Specify the name of the user to create: (i.e. Bob )")

If sUser = "" Then
  MsgBox "No user name was specified. You must specify a user name."
  Exit Sub
End If

If sComputer = "" Then
  On Error GoTo 0
  MsgBox "No computer or domain was specified. Script will use the current user's domain " & WshNetwork.UserDomain & "."
  Exit Sub
Else
  sPrefix = "LDAP://" & sComputer & "/"
End If

If sContainer = "" Then
  WScript.Echo "No container was specified. Script will use the Users container in the specified domain."
  Set IADsRootDSE = GetObject(sPrefix & "rootDSE")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject method"
  End If
  sDomain = IADsRootDSE.Get("defaultNamingContext")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Get method"
  End If
  sContainerDN = "cn=Users," + sDomain
Else
  sContainerDN = sContainer
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the container
'''''''''''''''''''''''''''''''''''''''
Set cont = GetObject(sPrefix & sContainerDN)
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on GetObject method"
End If
'''''''''''''''''''''''''''''''''''''''
'Add the user
'''''''''''''''''''''''''''''''''''''''
Set user = cont.Create("user", "cn=" & sUser)
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Create method"
End If
user.put "samAccountName", sUser
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Put samAccountName method"
End If
user.SetInfo
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on SetInfo method"
End If
strText = "The user " & sUser & " was successfully added."
strText = strText & vbCrLf & "The user has the following properties:"
'Refresh the property cache
user.GetInfo
'zz Count = user.PropertyCount
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on PropertyCount method"
End If
strText = strText & "Number of properties: " & Count

For cprop = 1 To Count
  Set v = user.Next()
  If IsNull(v) Then
    Exit For
  End If
  strText = strText & vbCrLf & cprop & ") " & v.Name & " (" & v.ADsType & ") "
Next
show_items strText, sComputer
strText = "User operational attributes"
user.GetInfoEx Array("canonicalName", "allowedAttributes", "allowedAttributesEffective"), 0
  strText = strText & vbCrLf & "Canonical Name:" & user.Get("canonicalName")
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on Get method"
End If
show_items strText, sComputer

cattr = 0
strText = "Attributes Allowed"
attr = user.GetEx("allowedAttributes")
For Each attrval In attr
  cattr = cattr + 1
Next
strText = strText & vbCrLf & cattr
show_items strText, sComputer

cattr = 0
strText = "Attributes Effective"
attr = user.GetEx("allowedAttributesEffective")
For Each attrval In attr
  cattr = cattr + 1
Next
strText = strText & vbCrLf & cattr
show_items strText, sComputer
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Create User on " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Deleting a User

To delete a user from a domain, you will need to bind to the container containing the user and specify "user" as the class using IADsContainer::Delete. Note that you do not need to call IADs::SetInfo to commit the change to the container. The IADsContainer::Delete call commits the deletion of the user directly to the directory.

Using C/C++, you can also bind to the container and use the IDirectoryObject::DeleteDSObject method.


Example Code for Deleting a User

[C++]

The following code fragment deletes a user in a domain:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  DeleteADObject()   - Deletes the passed object by AdsPath

    Parameters

        LPOLESTR pwszAdsPath        -       AdsPath of object to delete

    Optional Parameters:

       LPOLESTR pwszUser            - User Name and Password, if the parameters are NOT passed,
       LPOLESTER pwszPassWord       - Binding will use ADsGetObject, if the parameters
                                    - Are specified, will use ADsOpenObject, passing user name and password

*/
HRESULT DeleteADObject(LPOLESTR pwszAdsPath, LPOLESTR  pwszUser,LPOLESTR  pwszPassWord)
{
    HRESULT             hr;
    BSTR                bsParentPath;
    IADs *              pIADsToDelete = NULL;
    IDirectoryObject *  pIDirObjectParent= NULL;
    VARIANT             vCNToDelete;
    WCHAR               pwszTemp[512];

    VariantInit(&vCNToDelete);
    // Bind to the object being deleted

    assert((pwszUser==NULL && pwszPassWord == NULL) || (pwszUser && pwszPassWord));

    // If a username and password are passed in, use ADsOpenObject()
    // otherwise use ADsGetObject()
    if (!pwszUser) // No user password passed, use ADsOpenObject
    {
        hr = ADsGetObject(  pwszAdsPath, IID_IADs,(void **)& pIADsToDelete);
    }
    else
    {
        hr = ADsOpenObject(pwszAdsPath, pwszUser, pwszPassWord,
                           ADS_SECURE_AUTHENTICATION,IID_IADs, (void**) & pIADsToDelete);
    }

    if (SUCCEEDED(hr))
    {
       // Get the parent path
        hr = pIADsToDelete->get_Parent(&bsParentPath);

        // Get the CN property for the object to delete
        hr = pIADsToDelete->Get(L"cn",&vCNToDelete);

        if (SUCCEEDED(hr))
        {
            // ************************************************************
            // Now bind to the parent
            // If a username and password are passed in, use ADsOpenObject()
            // otherwise use ADsGetObject()
            if (!pwszUser) // No user password passed, use ADsOpenObject
            {
                hr = ADsGetObject(  bsParentPath, IID_IDirectoryObject,(void **)& pIDirObjectParent);
            }
            else
            {
                hr = ADsOpenObject(bsParentPath, pwszUser, pwszPassWord,
                                   ADS_SECURE_AUTHENTICATION,IID_IDirectoryObject, (void**) & pIDirObjectParent);
            }
            if (SUCCEEDED(hr))
            {
                // Release the object to delete
                pIADsToDelete->Release();
                pIADsToDelete =NULL;

                // Put the CN property into a string beginning with CN=
                swprintf(pwszTemp,L"cn=%s\n",vCNToDelete.bstrVal);

                // Ask the parent to delete the child
                hr =pIDirObjectParent->DeleteDSObject(pwszTemp);
                // Release the Parent Object
                pIDirObjectParent->Release();
                pIDirObjectParent = NULL;
            }
        }
        SysFreeString(bsParentPath);
    }
    // If there is an IADs Object, release it
    if ( pIADsToDelete)
    {
        // Release the object to delete
        pIADsToDelete->Release();
        pIADsToDelete =NULL;
    }

    VariantClear(&vCNToDelete);
 return hr;
}
[Visual Basic

The following code deletes a user in a domain:

Set x = GetObject("LDAP://OU=myou,DC=arcadiabay,DC=com")
x.Delete("user", "cn=userguy")


Enumerating Users

Unlike Windows NT® 4.0 domains, Windows® 2000 users can be placed in any container or organizational unit in a domain as well as the root of the domain. This means that users can be in numerous locations in the directory hierarchy. Therefore, you have two choices for enumerating users:

  1. Enumerate the users directly contained in a container, OU, or at the root of the domain.

    Explicitly bind to the container object containing the users you are interested in enumerating, set a filter containing "user" as the class using the IADsContainer::put_Filter method, and use the IADsContainer::get__NewEnum method to enumerate the user objects.

    Note this technique is useful if you want to enumerate users that are directly contained in a container or OU object. If the container contains other containers that can potentially contain other users, you need to bind to those containers and recursively enumerate the users on those containers. If you do not need to manipulate the user objects and only need to read specific properties, you should use the deep search described in option 2.

    Because enumeration returns pointers to ADSI COM objects representing each user object, you can call QueryInterface to get IADs, IADsUser, and IADsPropertyList interface pointers to the user object. This means you can get interface pointers to each enumerated user object in a container without having to explicitly bind to each user object. If you wanted to perform operations on all the users directly within a container, enumeration saves you from having to bind to each user in order to call IADs or IADsUser methods. If you only want to retrieve specific properties from users, you should use IDirectorySearch as described in option 2.

  2. Perform a deep search for (&(objectClass=user)(objectCategory=person)) to find all users in a tree.

    First, bind to the container object where you want to begin the search. For example, if you wanted to find all users in a domain, you would bind to root of the domain; if you wanted to find all users in the forest, you would bind to the global catalog and search from the root of the GC.

    Then use IDirectorySearch to query using a search filter containing (&(objectClass=user)(objectCategory=person)) and search preference of ADS_SCOPE_SUBTREE.

    Note that you can perform a search with a search preference of ADS_SCOPE_ONELEVEL to limit the search to the direct contents of the container object that you bound to.

    IDirectorySearch retrieves only the values of specific properties from users. If you only want to retrieve values, you should use IDirectorySearch. If you want to manipulate the user objects returned from a search (that is, you want to use IADs or IADsUser methods), you must explicitly bind to them (to do this, specify distinguishedName as one of the properties to return from the search and use the returned distinguished names to bind to each user returned in the search).

    Note that only specific properties are retrieved. You cannot retrieve all attributes without explicitly specifying every possible attribute of the user class.


Querying for Users

To query for a user, the query must contain the following search expression:

(&(objectClass=user)(objectCategory=person))

Why? Because the computer class is a subclass of user, a query containing only (objectClass=user) would return user objects and computer objects. Also, the object category of the user object is person (not user); therefore, the expression (objectCategory=user) will not return any users. If you used the expression (objectCategory=user), the query would return user objects and contact objects.

Users can be placed in any container or organizational unit in a domain as well as the root of the domain. This means that users can be in numerous locations in the directory hierarchy. You can perform a deep search for (objectCategory=user) to find all users in a container, organizational unit, domain, domain tree, or forest—depending on the object that the IDirectorySearch pointer you're using is bound to.


Example Code for Using the Global Catalog to Find Users in a Forest

[Visual Basic]

The following code finds all users in the forest by querying the global catalog using ADO:

Dim Con As ADODB.Connection
Dim ocommand As ADODB.Command
Dim gc As IADs

On Error Resume Next
'Maximum number of items to list on a msgbox.
MAX_DISPLAY = 5

'ADO Connection object
Set Con = CreateObject("ADODB.Connection")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CreateObject"
  End If
Con.Provider = "ADsDSOObject"
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Provider"
  End If
Con.Open "Active Directory Provider"
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Open"
  End If
'ADO Command object
Set ocommand = CreateObject("ADODB.Command")
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CreateObject"
  End If
ocommand.ActiveConnection = Con
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Active Connection"
  End If
Set gc = GetObject("GC:")
For Each child In gc
    Set entpr = child
Next
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on GetObject for GC"
  End If
show_items entpr.ADsPath, "foo"

ocommand.CommandText = "<" & entpr.ADsPath & ">;(&(objectCategory=person)(objectClass=user));distinguishedName,name;subTree"
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on CommandText"
  End If
Set rs = ocommand.Execute
  If (Err.Number <> 0) Then
     BailOnFailure Err.Number, "on Execute"
  End If
strText = "Found " & rs.RecordCount & " Users in Forest:"
intNumDisplay = 0
intCount = 0
' Navigate the record set
rs.MoveFirst
While Not rs.EOF
    intCount = intCount + 1
    strText = strText & vbCrLf & intCount & ") "
    For i = 0 To rs.Fields.Count - 1
        If rs.Fields(i).Type = adVariant And Not (IsNull(rs.Fields(i).Value)) Then
          strText = strText & rs.Fields(i).Name & " = "
          For j = LBound(rs.Fields(i).Value) To UBound(rs.Fields(i).Value)
             strText = strText & rs.Fields(i).Value(j) & " "
          Next
        Else
          strText = strText & rs.Fields(i).Name & " = " & rs.Fields(i).Value & vbCrLf
        End If
    Next
    intNumDisplay = intNumDisplay + 1
    'Display in msgbox if there are MAX_DISPLAY items to display
    If intNumDisplay = MAX_DISPLAY Then
        Call show_items(strText, "Users in forest")
        strText = ""
        intNumDisplay = 0
    End If
    rs.MoveNext
Wend
show_items strText, "foo"
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Search GC for users" & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub
[C++]

The following code fragment contains a function that finds all users in the forest by querying the global catalog:

HRESULT FindAllUsersInGC()
{
    HRESULT hr = E_FAIL;
    HRESULT hrGC = S_OK;

    VARIANT var;
    ULONG lFetch;

    // Interface Pointers
    IDirectorySearch *pGCSearch = NULL;
    IADsContainer *pContainer = NULL;
    IUnknown *pUnk = NULL;
    IEnumVARIANT *pEnum = NULL;
       IDispatch *pDisp = NULL;
    IADs *pADs = NULL;

    //Bind to global catalog
    hr = ADsOpenObject(L"GC:",
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IADsContainer,
                (void**)&pContainer);

    if (SUCCEEDED(hr))
    {
       hr = pContainer->get__NewEnum( &pUnk );
        if (SUCCEEDED(hr))
        {
            hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
            if (SUCCEEDED(hr))
            {
            // Now Enumerate--there should be only one item.
            hr = pEnum->Next( 1, &var, &lFetch );
            if (SUCCEEDED(hr))
            {
                while( hr == S_OK )
                {
                    if ( lFetch == 1 )
                    {
                        pDisp = V_DISPATCH(&var);
                        hr = pDisp->QueryInterface( IID_IDirectorySearch, (void**)&pGCSearch);
                        hrGC = hr;
                    }
                    VariantClear(&var);
                    hr = pEnum->Next( 1, &var, &lFetch );
                };
            }
        }
        if (pEnum)
            pEnum->Release();
        }
        if (pUnk)
        pUnk->Release();
    }
    if (pContainer)
        pContainer->Release();

    if (FAILED(hrGC))
    {
        if (pGCSearch)
            pGCSearch->Release();
        return hrGC;
    }

    //Create search filter
    LPOLESTR pszSearchFilter = L"(&(objectCategory=person)(objectClass=user))";
    //Search entire subtree from root.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_SUBTREE;
    DWORD dwNumPrefs = 1;

    // COL for iterations
    ADS_SEARCH_COLUMN col;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;

    // Set the search preference
    hr = pGCSearch->SetSearchPreference( &SearchPrefs, dwNumPrefs);
    if (FAILED(hr))
        return hr;
    // Set attributes to return
    CONST DWORD dwAttrNameSize = 2;
    LPOLESTR pszAttribute[dwAttrNameSize] = {L"cn",L"distinguishedName"};

    // Execute the search
    hr = pGCSearch->ExecuteSearch(pszSearchFilter,
                                  pszAttribute,
                                    dwAttrNameSize,
                                    &hSearch
                                    );
    if ( SUCCEEDED(hr) )
    {

    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pGCSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {

            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {

                // Get the data for this column
                hr = pGCSearch->GetColumn( hSearch, pszAttribute[x], &col );

                if ( SUCCEEDED(hr) )
                {
                    // Print the data for the column and free the column
                    // Note the requested attributes are type CaseIgnoreString.
                    wprintf(L"%s: %s\r\n",pszAttribute[x],col.pADsValues->CaseIgnoreString);
                    pGCSearch->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property is not a string>",pszAttribute[x]);
            }
            wprintf(L"------------------------------\n");
        }

        // Close the search handle to clean up
        pGCSearch->CloseSearchHandle(hSearch);
    }
    if (pGCSearch)
        pGCSearch->Release();
    return hr;
}


Moving Users

Users can be moved within a domain.

However, you can only move objects between domains using the movetree.exe utility. Note that when a user is moved to a different domain, the objectSID is changed to reflect the new domain and the old objectSID is added as a value to the sIDHistory property. The objectGUID remains the same.

To move a user

  1. Bind to the user to move and get an IADs pointer.
  2. Get the ADsPath using the IADs::get_ADsPath method. The ADsPath will be used to specify the user to move.
  3. Bind to the container object where you want to move the user to and get an IADsContainer pointer.
  4. Move the user using the IADsContainer::MoveHere method.

    Note that if you have a pointer to the user object before it was moved, the pointer to the object is still valid but the object's methods will no longer be valid since the directory object it represents is no longer valid.

For sample code for moving objects, see Moving Groups.


Managing Users on Member Servers and Windows 2000 Professional

On member servers and Windows® 2000 Professional, there is a local security database. That local security database can contain its own local users whose scope is only the particular computer where they are created. When managing these types of users on member servers and workstations, you use the WinNT provider.

When managing users on a Windows 2000 domain using ADSI, you use the LDAP provider. When managing users on member servers and workstations, you use the WinNT provider.


Enumerating Users on Member Servers and Windows 2000 Professional

On member servers and computers running Windows 2000 Professional, you can easily enumerate all the users in local security database.

To enumerate the users on a member server or computer running Windows 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName, computer

      where sComputerName is the name of the computer who groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer and allows the WinNT: provider's parser to skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Set a filter containing "user" using the IADsContainer::put_Filter method. This enables you to enumerate the container and retrieve only users.
  3. Enumerate the user objects, using the IADsContainer::get__NewEnum method.
  4. For each the user object, using the IADs or IADsUser methods to read the properties of the user.


Example Code for Enumerating Users on a Member Server or Windows 2000 Professional

[Visual Basic]

The following script enumerates all users on a member server or Windows 2000 Professional:

Dim cont As IADsContainer

'Example: Enumerating all local users on member server or workstation
'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
sComputer = InputBox("This script lists the users on a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name:")

If sComputer = "" Then
  Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
'Note that this sample uses the logged-on user's context
'You should use IADsOpenDSObject to use a specific account
'that is not the logged-on user's account.
Set cont = GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Filter to view only user objects
'''''''''''''''''''''''''''''''''''''''
cont.Filter = Array("User")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsContainer::Filter method"
End If

strText = ""
intIndex = 0
intNumDisplay = 0
cmember = 0
'Maximum number of users to list on a msgbox.
MAX_DISPLAY = 20

'''''''''''''''''''''''''''''''''''''''
'Get each user
'''''''''''''''''''''''''''''''''''''''
For Each user In cont
    intIndex = intIndex + 1
    'Get the name
    strText = strText & vbCrLf & Right("  " & intIndex, 4) & " " & user.Name
    intNumDisplay = intNumDisplay + 1
    'Display in msgbox if there are MAX_DISPLAY users to display
    If intNumDisplay >= MAX_DISPLAY Then
        Call show_users(strText, sComputer)
        strText = ""
        intNumDisplay = 0
    End If
    'Reset the count of members within the current group
    cmember = 0
Next
Call show_users(strText, sComputer)
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_users(strText, strName)
    MsgBox strText, vbInformation, "Users on " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub
[C++]

The following code fragment contains a function that enumerates all objects of a specified class (such as user) and displays the members contained in each object on a member server or Windows 2000 Professional:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  ListObjectsWithWinNtProvider()    - Uses the WinNT provider to list children based on a filter
                                        Returns S_OK on success

    Parameters

    LPWSTR pwszComputer     - Computer to list
    LPWSTR pwszClass        - Filter for listing
    LPWSTR pwszUSER = NULL  - User Name for ADsOpenObject() binding- If NOT passed - Bind Though ADsGetObject()
    LPWSTR pwszPASS = NULL  - Password for ADsOpenObject() binding- If NOT passed - Bind Though ADsGetObject()

*/
HRESULT ListObjectsWithWinNtProvider(LPWSTR pwszComputer,LPWSTR pwszClass, LPWSTR pwszUSER = NULL, LPWSTR pwszPASS = NULL)
{
    HRESULT hr;
    LPWSTR  pwszBindingString = NULL;

    IADsContainer * pIADsCont = NULL;

    // Allocate a String for Binding.. This should definitely be big enough..
    pwszBindingString = new WCHAR[(wcslen(gbsComputer) *2) + 20];

    swprintf(pwszBindingString,L"WinNT://%s,computer",pwszComputer);

    // Make sure either NO user is passed - or BOTH user and password are passed
    assert(!pwszUSER || (pwszUSER && pwszPASS));

    // Bind to the container passed
    // If USER and PASS passed in, use ADsOpenObject()
    if (pwszUSER)
        hr = ADsOpenObject( pwszBindingString,
                            pwszUSER,
                            pwszPASS,
                            ADS_SECURE_AUTHENTICATION,
                            IID_IADsContainer,
                            (void**) &pIADsCont);
    else
        hr = ADsGetObject( pwszBindingString, IID_IADsContainer,(void **)&pIADsCont);

    if (SUCCEEDED(hr))
    {
        VARIANT vFilter;
        VariantInit(&vFilter);
        LPWSTR pwszFilter = pwszClass;

        // Build a Variant of array type, using the filter passed
        hr = ADsBuildVarArrayStr(&pwszFilter, 1, &vFilter);
        if (SUCCEEDED(hr))
        {
            // Set the filter for the results of the Enum
            hr = pIADsCont->put_Filter(vFilter);
            if (SUCCEEDED(hr))
            {
                IEnumVARIANT * pEnumVariant = NULL; // Ptr to the IEnumVariant Interface
                VARIANT Variant;                    // Variant for retrieving data
                ULONG   ulElementsFetched;          // Number of elements fetched

                // Builds an enumerator interface- this will be used
                // to enumerate the objects contained in the IADsContainer
                hr = ADsBuildEnumerator(pIADsCont,&pEnumVariant);
                // While no errors- Loop through and print the data
                while (SUCCEEDED(hr) && hr != S_FALSE)
                {
                    // Object comes back as a VARIANT holding an IDispatch *
                    hr = ADsEnumerateNext(pEnumVariant,1,&Variant,&ulElementsFetched);
                    if (hr != S_FALSE)
                    {
                        assert(HAS_BIT_STYLE(Variant.vt,VT_DISPATCH));
                        IDispatch *pDispatch = NULL;
                        IADs *pIADs= NULL;
                        pDispatch = Variant.pdispVal;
                        // QI the Variant's IDispatch * for the IADs interface
                        hr = pDispatch->QueryInterface(IID_IADs,(VOID **) &pIADs) ;

                        if (SUCCEEDED(hr))
                        {
                            // Print some information about the object
                            BSTR bsResult;
                            pIADs->get_Name(&bsResult);
                            wprintf(L" NAME: %s\n",(LPOLESTR) bsResult);
                            SysFreeString(bsResult);

                            pIADs->get_ADsPath(&bsResult);
                            wprintf(L" ADSPATH: %s\n",(LPOLESTR) bsResult);
                            SysFreeString(bsResult);

                            puts("------------------------------------------------------");
                            pIADs->Release();
                            pIADs = NULL;
                        }
                    }
                }

                // Since the hr from iteration was lost, free
                // the interface if the ptr is != NULL
                if (pEnumVariant)
                {
                    pEnumVariant->Release();
                    pEnumVariant = NULL;
                }
                VariantClear(&Variant);
            }
        }
        VariantClear(&vFilter);
    }

    delete [] pwszBindingString;
    pwszBindingString = NULL;

    return hr;
}


Creating Users on Member Servers and Windows 2000 Professional

To create a user on a member server or computer running Windows 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName, computer

      where sComputerName is the name of the computer who groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer and allows the WinNT: provider's parser to skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Specify "user" as the class using IADsContainer::Create to add the user.
  3. Write the user to the computer's security database using IADs::SetInfo.


Example Code for Creating Users on a Member Server or Windows 2000 Professional

The following Visual Basic code creates a user on a member server or Windows 2000 Professional:

'Example: Creating a user on a member server or workstation

Dim cont As IADsContainer
Dim oUser As IADsUser
Dim v As Variant

'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
On Error Resume Next

sComputer = InputBox("This script creates a user on a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name:")
sUser = InputBox("Specify the user name:")

If sComputer = "" Then
    Exit Sub
End If
If sUser = "" Then
    Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
Set cont = GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Create the user
'''''''''''''''''''''''''''''''''''''''
Set oUser = cont.Create("user", sUser)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsContainer::Create method"
End If

'''''''''''''''''''''''''''''''''''''''
'Write the user to the computer's security database.
'''''''''''''''''''''''''''''''''''''''
oUser.SetInfo
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADs::SetInfo method"
End If
'''''''''''''''''''''''''''''''''''''''
'Read the user that was just created
'and display its name and its properties.
'''''''''''''''''''''''''''''''''''''''
If (Err.Number <> 0) Then
  BailOnFailure Err.Number, "on SetInfo method"
End If
strText = "The user " & sUser & " was successfully added."
strText = strText & vbCrLf & "The user has the following properties:"
'Refresh the property cache
oUser.GetInfo

strText = strText & "Number of properties: " & Count
For cprop = 1 To Count
  Set v = oUser.Next()
  If IsNull(v) Then
    Exit For
  End If
  strText = strText & vbCrLf & cprop & ") " & v.Name & " (" & v.ADsType & ") "
Next
show_items strText, sComputer
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_items(strText, strName)
    MsgBox strText, vbInformation, "Create user on " & strName
End Sub


Deleting Users on Member Servers and Windows 2000 Professional

To delete a user from a member server or computer running Windows® 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName, computer

      In this example, sComputerName is the name of the computer who groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer and allows the WinNT: provider's parser to skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Specify "user" as the class using IADsContainer::Delete to delete the user.

    Note that you do not need to call IADs::SetInfo to commit the change to the container. The IADsContainer::Delete call commits the deletion of the user directly to the directory.


Example Code for Deleting Users on a Member Server or Windows 2000 Professional

The following Visual Basic code deletes a user on a member server or workstation:

Dim cont As IADsContainer
Dim oGroup As IADsGroup

'Example: Deleting a user on a member server or workstation

'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
On Error Resume Next
sComputer = InputBox("This script deletes a user from a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name:")
If sComputer = "" Then
    Exit Sub
End If

sUser = InputBox("Specify the user name:")
If sUser = "" Then
    Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
Set cont = GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Delete the user
'''''''''''''''''''''''''''''''''''''''
cont.Delete "user", sUser

If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsContainer::Delete method"
End If

strText = "The user " & sUser & " was deleted on computer " & sComputer & "."

Call show_users(strText, sComputer)
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_users(strText, strName)
    MsgBox strText, vbInformation, "Delete user on " & strName
End Sub


Values for countryCode

The following table lists the country codes for the countryCode property and the name displayed by Active Directory™ Users and Computers.

AFGHANISTAN | AF
ALBANIA | AL
ALGERIA | DZ
AMERICAN SAMOA | AS
ANDORRA | AD
ANGOLA | AO
ANGUILLA | AI
ANTARCTICA | AQ
ANTIGUA AND BARBUDA | AG
ARGENTINA | AR
ARMENIA | AM
ARUBA | AW
AUSTRALIA | AU
AUSTRIA | AT
AZERBAIJAN | AZ
BAHAMAS | BS
BAHRAIN | BH
BANGLADESH | BD
BARBADOS | BB
BELARUS | BY
BELGIUM | BE
BELIZE | BZ
BENIN | BJ
BERMUDA | BM
BHUTAN | BT
BOLIVIA | BO
BOSNIA AND HERZEGOWINA | BA
BOTSWANA | BW
BOUVET ISLAND | BV
BRAZIL | BR
BRITISH INDIAN OCEAN TERRITORY | IO
BRUNEI DARUSSALAM | BN
BULGARIA | BG
BURKINA FASO | BF
BURUNDI | BI
CAMBODIA | KH
CAMEROON | CM
CANADA | CA
CAPE VERDE | CV
CAYMAN ISLANDS | KY
CENTRAL AFRICAN REPUBLIC | CF
CHAD | TD
CHILE | CL
CHINA | CN
CHRISTMAS ISLAND | CX
COCOS (KEELING) ISLANDS | CC
COLOMBIA | CO
COMOROS | KM
CONGO | CG
CONGO, THE DEMOCRATIC REPUBLIC OF THE | CD
COOK ISLANDS | CK
COSTA RICA | CR
COTE D'IVOIRE | CI
CROATIA (local name: Hrvatska) | HR
CUBA | CU
CYPRUS | CY
CZECH REPUBLIC | CZ
DENMARK | DK
DJIBOUTI | DJ
DOMINICA | DM
DOMINICAN REPUBLIC | DO
EAST TIMOR | TP
ECUADOR | EC
EGYPT | EG
EL SALVADOR | SV
EQUATORIAL GUINEA | GQ
ERITREA | ER
ESTONIA | EE
ETHIOPIA | ET
FALKLAND ISLANDS (MALVINAS) | FK
FAROE ISLANDS | FO
FIJI | FJ
FINLAND | FI
FRANCE | FR
FRANCE, METROPOLITAN | FX
FRENCH GUIANA | GF
FRENCH POLYNESIA | PF
FRENCH SOUTHERN TERRITORIES | TF
GABON | GA
GAMBIA | GM
GEORGIA | GE
GERMANY | DE
GHANA | GH
GIBRALTAR | GI
GREECE | GR
GREENLAND | GL
GRENADA | GD
GUADELOUPE | GP
GUAM | GU
GUATEMALA | GT
GUINEA | GN
GUINEA-BISSAU | GW
GUYANA | GY
HAITI | HT
HEARD AND MC DONALD ISLANDS | HM
HOLY SEE (VATICAN CITY STATE) | VA
HONDURAS | HN
HONG KONG | HK
HUNGARY | HU
ICELAND | IS
INDIA | IN
INDONESIA | ID
IRAN (ISLAMIC REPUBLIC OF) | IR
IRAQ | IQ
IRELAND | IE
ISRAEL | IL
ITALY | IT
JAMAICA | JM
JAPAN | JP
JORDAN | JO
KAZAKHSTAN | KZ
KENYA | KE
KIRIBATI | KI
KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF | KP
KOREA, REPUBLIC OF | KR
KUWAIT | KW
KYRGYZSTAN | KG
LAO PEOPLE'S DEMOCRATIC REPUBLIC | LA
LATVIA | LV
LEBANON | LB
LESOTHO | LS
LIBERIA | LR
LIBYAN ARAB JAMAHIRIYA | LY
LIECHTENSTEIN | LI
LITHUANIA | LT
LUXEMBOURG | LU
MACAU | MO
MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF | MK
MADAGASCAR | MG
MALAWI | MW
MALAYSIA | MY
MALDIVES | MV
MALI | ML
MALTA | MT
MARSHALL ISLANDS | MH
MARTINIQUE | MQ
MAURITANIA | MR
MAURITIUS | MU
MAYOTTE | YT
MEXICO | MX
MICRONESIA, FEDERATED STATES OF | FM
MOLDOVA, REPUBLIC OF | MD
MONACO | MC
MONGOLIA | MN
MONTSERRAT | MS
MOROCCO | MA
MOZAMBIQUE | MZ
MYANMAR | MM
NAMIBIA | NA
NAURU | NR
NEPAL | NP
NETHERLANDS | NL
NETHERLANDS ANTILLES | AN
NEW CALEDONIA | NC
NEW ZEALAND | NZ
NICARAGUA | NI
NIGER | NE
NIGERIA | NG
NIUE | NU
NORFOLK ISLAND | NF
NORTHERN MARIANA ISLANDS | MP
NORWAY | NO
OMAN | OM
PAKISTAN | PK
PALAU | PW
PANAMA | PA
PAPUA NEW GUINEA | PG
PARAGUAY | PY
PERU | PE
PHILIPPINES | PH
PITCAIRN | PN
POLAND | PL
PORTUGAL | PT
PUERTO RICO | PR
QATAR | QA
REUNION | RE
ROMANIA | RO
RUSSIAN FEDERATION | RU
RWANDA | RW
SAINT KITTS AND NEVIS | KN
SAINT LUCIA | LC
SAINT VINCENT AND THE GRENADINES | VC
SAMOA | WS
SAN MARINO | SM
SAO TOME AND PRINCIPE | ST
SAUDI ARABIA | SA
SENEGAL | SN
SEYCHELLES | SC
SIERRA LEONE | SL
SINGAPORE | SG
SLOVAKIA (Slovak Republic) | SK
SLOVENIA | SI
SOLOMON ISLANDS | SB
SOMALIA | SO
SOUTH AFRICA | ZA
SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS | GS
SPAIN | ES
SRI LANKA | LK
ST. HELENA | SH
ST. PIERRE AND MIQUELON | PM
SUDAN | SD
SURINAME | SR
SVALBARD AND JAN MAYEN ISLANDS | SJ
SWAZILAND | SZ
SWEDEN | SE
SWITZERLAND | CH
SYRIAN ARAB REPUBLIC | SY
TAIWAN | TW
TAJIKISTAN | TJ
TANZANIA, UNITED REPUBLIC OF | TZ
THAILAND | TH
TOGO | TG
TOKELAU | TK
TONGA | TO
TRINIDAD AND TOBAGO | TT
TUNISIA | TN
TURKEY | TR
TURKMENISTAN | TM
TURKS AND CAICOS ISLANDS | TC
TUVALU | TV
UGANDA | UG
UKRAINE | UA
UNITED ARAB EMIRATES | AE
UNITED KINGDOM | GB
UNITED STATES | US
UNITED STATES MINOR OUTLYING ISLANDS | UM
URUGUAY | UY
UZBEKISTAN | UZ
VANUATU | VU
VENEZUELA | VE
VIET NAM | VN
VIRGIN ISLANDS (BRITISH) | VG
VIRGIN ISLANDS (U.S.) | VI
WALLIS AND FUTUNA ISLANDS | WF
WESTERN SAHARA | EH
YEMEN | YE
YUGOSLAVIA | YU
ZAMBIA | ZM
ZIMBABWE | ZW


Managing Groups

This section details what you need to know to use groups in Active Directory™. It discusses the following:

  • What are groups and how are they used
  • Group objects
  • Groups on mixed- and native-mode domains
  • Creating groups
  • Adding members to groups
  • Removing members from groups in a domain
  • Nesting groups
  • Determining a user's or group's membership in a group
  • Enumerating groups
  • Querying for groups in a domain
  • Changing a group's scope or type
  • Deleting groups
  • Moving groups
  • Getting the domain account-style name of a group
  • Managing groups on member servers and a computer running Windows NT Workstation/Windows 2000 Professional
  • What application and service developers need to know about groups


Groups in Active Directory

Groups are Active Directory™ or local computer objects that can contain users, contacts, computers, and other groups. Groups can be used to do the following:

  • Manage user and computer access to shared resources such as Active Directory objects and their properties, network shares, files, directories, printer queues, and so on.
  • Create e-mail distribution lists.
  • Filter group policy.

Groups can be used for security purposes (such as access control and policy) or they can be used for grouping purposes (such as distribution lists). Specify whether a group is used for security purposes when you create the group.

When assigning permissions for resources (file shares, printers, and so on), administrators should assign those permissions to a group rather than to the individual users. The permissions are assigned once to the group, instead of several times to each individual user. This helps simplify the maintenance and administration of a network.

To control access to a frequently used resource, create a group that will contain users that require that type of access, add one or more ACEs to set the access for that group on the security descriptor for the resource, and then add any users who require that type of access to the resource as members of the group. For more information about setting access on directory objects, see Access Control. For more information about setting access on other objects in Windows® 2000, see Setting Access Rights on an Entire Object.

When a user is made a member of a group, that user is given all the rights and permissions granted to the group. However, if the user is already logged on, the rights of the newly assigned group will not take effect until he/she logs off and logs on again.

Contacts in a group can be sent e-mail, but cannot be assigned rights and permissions. Note that although a contact can be added to a security group as well as to a distribution group, contacts cannot be used to assign rights and permissions.

Groups vs. Organizational Units

[This is preliminary documentation and subject to change.]

Groups are distinct from organizational units (OUs). OUs are useful for creating a hierarchy for administrative delegation or setting group policy. Groups are used for granting access and creating distribution lists.

Groups and organizational units also differ in regard to the domain boundaries to which they are applied. You can create groups to contain users, computers, or shared resources on a local server, a single domain, or multiple domains in a forest. Organizational units represent a collection of objects (including group objects) only within the context of a single domain.

Native vs. Mixed Mode

[This is preliminary documentation and subject to change.]

In Windows® 2000, domains can operate in two different modes:

  • Mixed Mode. Windows NT® 4.0 domain controllers may be present in this environment. This is the default mode when a domain is created. Clients and member servers may be still running previous versions of Windows NT, Windows 98, or Windows 95.
  • Native Mode. All domain controllers must be running a Windows 2000 Server. Clients and member servers may still be running Windows NT 4.0. Clients and member servers may still be running previous versions of Windows NT, Windows 98, or Windows 95.

A domain must be in native mode to use the following Windows 2000 group features:

  • Universal security groups. Universal groups are a new type of group that can be created and used in any domain in the forest.
  • Nesting security groups.
  • Conversion of groups.

Mixed mode supports all types of distribution groups (including Universal) and nesting of distribution groups. Mixed mode should only be used to support Windows NT 4.0 domain controllers during the migration process. Note that a domain tree or forest can contain both mixed mode and native-mode domains.

Before creating or converting groups that require native mode, your application should check the operation mode of the domain.

Nesting

[This is preliminary documentation and subject to change.]

In Windows 2000, groups can contain other groups. This is called nesting. Note that nesting is supported only for distribution groups in domains running in mixed mode. A domain must be in native mode to nest security groups (as well as distribution groups).

Nesting can be an efficient way to handle large memberships as well as delegate management of group membership. For example, the top group could be a universal group that contains only global groups. The domain administrators of the domains containing those global groups can manage the membership within their own domains. The enterprise administrator can simply manage the global group membership of the universal group (that is, adding and removing global groups) and let the domain administrators handle the membership requests from users in their own domain.


Types of Groups

A group may be a security group or a distribution group.

Security groups are listed in access-control lists (ACLs), which define permissions on resources and objects. Security groups can also be used as an e-mail entity. Sending an e-mail message to the group sends the message to all the members of the group.

Distribution groups are not security enabled. Distribution groups provide a subset of the functionality of security groups. Distribution groups cannot be listed in ACLs. Distribution groups can be used only for grouping purposes. For example, distribution lists can be used with e-mail applications (such as Exchange) to send e-mail to collections of users.

If you do not need a group for security purposes, create a distribution group instead. Using a distribution group improves performance because distribution groups are ignored when Windows 2000 builds the user security token during the logon process. This also reduces the size of the token, improving performance as the token is sent to various computers the user accesses.


How Security Groups are Used in Access Control

The security identifier (SID) is the object identifier of the user or security group when the user or group is used for security purposes. The name of the user or group is not used as the unique identifier within the system. The SID is stored in the objectSid property of user objects and security group objects. Active Directory generates the objectSid when the user or group is created. The system ensures that the SIDs are unique across a forest. Note that the objectGuid is the unique identifier of a user, group, or any other directory object. The SID changes if a user or group is moved to another domain; the objectGuid always remains the same.

When a user or group is given permission to access a resource (such as a printer or a file share), the SID of the user or group is added to the access control entry (ACE) defining the granted permission in the resource's discretionary access control list (DACL). In Active Directory, each object has an nTSecurityDescriptor property that stores a DACL defining the access to that particular object or properties on that object. For more information about setting access control on objects in Active Directory, see Access Control on Active Directory Objects.

When a user logs on to a Windows 2000 domain, the operating system generates an access token. This access token is used when determining which resources the user may access. Among other information, the user's access token includes the following:

  • User's SID
  • SIDs of the global and universal security groups of which the user is a member

Every process executed on behalf of this user has a copy of this access token.

When the user attempts to access resources on a computer, the service through which the user accesses the resource impersonates the user by creating a new access token based on the access token created at user logon time. This new access token additionally contains SIDs for all domain local groups that 1) are in the target resource's domain and 2) include the user. The service uses this new access token to evaluate access to the resource. If a SID in the access token appears in any ACEs in the DACL, the service gives the user the permissions specified in those ACEs.


Where Groups Can Be Created

In Active Directory, groups are created in domains. Groups can be created at the root of the domain (domainDNS), in an organizational unit (OU), or in a container (container) object.

The domain where you create a group can make a difference based on the scope of the group. The scope of a group determines 1) domains where members can be added from and 2) domains where the group can be used to grant permissions.

You should choose the particular container (domain, OU, or container) where you create a group based on the administration required for the group. For example, if your directory has multiple OUs, each of which has a different user to administer it, you may want to create global groups within those OUs so that those administrators can manage group membership for users in those OUs. If groups are required for access control outside the OU, the groups within the OUs can be nested in universal groups (or other global groups) that can be used elsewhere in the domain and forest.

Note that groups can be moved within a domain; however, only universal groups can be moved from one domain to another.


Scope of Groups

Each security and distribution group has a scope:

  • Domains from which members can be added to the group
  • Domains where the group can be used to grant permissions
  • Domains where the group can be nested in other groups.

There are three scopes for groups, as shown in the following table.

Scope | Members | Grant Permissions | Member of Other Groups
Universal | From any Windows NT/Windows 2000 domain in the forest: Universal Groups, Global Groups and users (including contacts) from any domain in the forest. | On any domain in the forest | Can be a member of the following groups in the forest: Local Groups and Universal Groups.
Global | Only from the domain containing the group: Global Groups and users (including contacts) from the domain containing the group. | On any domain in the forest | Can be a member of any group in the forest: Global Groups, Local Groups, and Universal Groups.
Domain Local | From any domain in the forest: Global Groups, Universal Groups, and users (including contacts) from any domain in the forest. Domain local groups from the domain containing the group | Only on the domain containing the group | Only can be a member of Local Groups in the domain containing the group.

If you have multiple forests, users from one forest cannot be placed in groups in another, and groups from one forest cannot be given permissions in another.

In short, a universal group can contain users and groups from any domain and can be used for access control in any domain. A global group can contain only users and groups from a single domain and can be used for access control on any domain. A domain local group can contain users and groups from any domain and can only be used for access control on a single domain.


Group Scope and the Global Catalog

Universal groups and their members are listed in the global catalog.

Global and domain local groups are also listed in the global catalog, but their members are not. This reduces the size of the global catalog, and dramatically reduces the replication traffic needed to keep the global catalog up-to-date.

Changes in membership in global and domain local groups have no effect on the global catalog. However, changes in membership in the


Effects of Universal Groups on the Global Catalog

For customers using universal groups in larger systems with slow connections, the following issues should be addressed to avoid performance and availability problems:

  • Authentication of a user requires global knowledge of the user's group memberships in order to compute all the groups that user (directly or indirectly) belongs to. With universal groups, the global catalog service (GC) is used to perform this membership computation. If a GC is not available at logon time, the user will not be able to log on to a native-mode domain. On a mixed-mode domain, the user may not have the appropriate access to resources according to universal group memberships in other domains.

    Solution: To avoid GC availability problems when using Universal groups, configure one or more GC servers per site.

  • Since authentication requires the GC to compute a user's universal group memberships, the GC must contain all universal group memberships. If only universal groups are used, a high level of GC replication traffic may be generated if the universal group memberships are changed frequently. A medium-sized branch office might not be able to afford the network bandwidth required to keep a GC up to date.

    Solution: To avoid unnecessary GC replication traffic with frequently changing group memberships, nest global groups within universal groups and make membership changes in the global groups. This leaves the universal group membership static (this avoids the need to replicate membership changes in the GC) and enables administrators to make changes frequently to global groups (global group membership is replicated only within the domain).


What Type of Group to Use

Which type of group should you create? When you create a group, you specify the type and scope. However, how you intend to use the group for access control also affects the type and scope.

Type

Groups use following types:

  • Security. Use security groups to control access to resources. Note that security groups can also be used as e-mail distribution lists.
  • Distribution. Use distribution groups if the group is only used for e-mail distribution lists or for grouping purposes. The primary purpose of this type of group is to group related objects together. Distribution groups cannot be used for access control. You can explicitly disable security on these groups to avoid unnecessary evaluation of these groups during a user's logon process. This avoids the expense of examining them when the logon process creates the user's security token (this makes logon more efficient) and reduces the size of the security token (this makes evaluation of access rights more efficient).

If the domain containing the group is running in native-mode, you can convert the type of a group after it has been created. The type cannot be converted in mixed-mode.

Scope

Groups use the following scopes:

  • Universal. Use universal groups to define functional groups that span domains. To do this, nest global groups within universal groups and make membership changes in the global groups. The universal group should change infrequently (add/remove global groups) while its membership might change much more frequently (add/remove members from the global groups). For example, define the group system administrators from ArcadiaBay.com as a universal group that contains the global groups sys-admins from northamericadom.arcadiabay.com, sys-admins from europedom.arcadiabay.com, and so on. Each of these account groups would contain the individual system administrators from a single domain.

    Note that universal groups require access to a global catalog server. For more information about the impact of universal groups on the global catalog, see Group Scope and the Global Catalog.

    Enterprises with a single domain should not use universal groups. Instead, use global groups and domain local groups as appropriate. Avoiding universal groups in the single domain scenario makes the transition easier if the enterprise subsequently expands to multiple domains.

  • Global. Use global groups for membership maintenance (adding and removing users) should occur within the global groups.

    If a domain contains a hierarchy of organizational units and administration is delegated to administrators at each OU, it may be simpler and more efficient to nest global groups. For example, if OU 1 contained OU 2 and OU 3, a global group in OU 1 could contain global groups in OU 2 and OU 3. In OU 1, the administrator would add/remove users from OU 1 and the administrators of OU 2 and 3 would add/remove members for users from their own OUs.

  • Domain Local. Use domain local groups to define access policies on resources within a domain. Depending upon the situation domain local groups will contain global groups, universal groups, individual accounts, other domain local groups, or a mixture of these.

Usage

If you intend a group to be used to set access rights on directory objects, you must create a group with Global scope. Why? All objects and their security descriptors (which control access to the object) are replicated to every global catalog server. The Configuration container (which contains objects that also have security descriptors) is also replicated to all DCs in the forest. If you use a domain local group to assign rights in a security descriptor, there are circumstances when the group will not be in the user's access token and, therefore, the right assigned will not be enforced. When the directory object is replicated to the Configuration container or global catalog in another domain and a user requests access to the object in that other domain, the DC handling the access request will not be able to add the domain local group to the access token. Effectively, the access that the assigned right containing the domain local group defines will not be enforced in these domains. The assigned right will only be enforced on the domain containing the domain local group.

Domain Local groups should only be used to manage resources that 1) are not stored in the directory (such as file shares, printer queues, and so on) and 2) are on computers in the domain containing the Domain Local group.


Group Objects

A group is represented as a group object in Active Directory. The group object has seven important properties:

cn
Is a property with a single value that is the object's relative distinguished name. The cn is the name of the group in Active Directory. As with all other objects, the cn of a group must be unique among the sibling objects in the container containing the group. member

Is a property with multiple properties that contains the list of distinguished names for the user, group, and contact objects that are members of the group. Each item in the list is a linked reference to the object that represents the member; therefore, Active Directory automatically updates the distinguished names in the member property when a member object is moved or renamed.

groupType
Is a property with a single value that is an integer that specifies the group type and scope using the following bit flags:

ADS_GROUP_TYPE_GLOBAL_GROUP

ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP

ADS_GROUP_TYPE_UNIVERSAL_GROUP

ADS_GROUP_TYPE_SECURITY_ENABLED

The first three flags specify the group scope.

The ADS_GROUP_TYPE_SECURITY_ENABLED flag indicates the type of the group. If this flag is set, the group is a security group. If this flag is not set, the group is a distribution group.

memberOf
Is a property with multiple values that contains the list of distinguished names for groups that contain the group as a member. This property lists the groups beneath which the group is directly nested — it does not contain the recursive list of nested predecessors. For example, if group D were nested in group C and group B and group B were nested in group A, the membersOf property of group D would list group C and group B but not group A.
objectGUID
Is a property with a single value that is the unique identifier for the object. This property is a GUID (Globally Unique Identifier). When an object is created in the directory, Active Directory generates a GUID and assigns it to the object's objectGUID property. The GUID is unique across the enterprise and anywhere else.

The objectGUID is a 128-bit GUID structure stored as an OctetString.

objectSid (Object-Sid)
Is a property with a single value that specifies the security identifier (SID) of the group. The SID is a unique value used to identify the group as a security principal. It is a binary value that the system sets when the group is created.

Each group has a unique SID that the Windows NT®/Windows® 2000 Server domain issues which is stored in the objectSid property of the group object in the directory. Each time a user logs on, the system retrieves the SID for the groups of which the user is a member and places it in the user's access token. The system uses the SIDs in the user's access token to identify the user and his/her group memberships in all subsequent interactions with Windows NT/Windows 2000 security.

When a SID has been used as the unique identifier for a user or group, it cannot ever be used again to identify another user or group.

sAMAccountName
Is a property with a single value that is the logon name used to support clients and servers from a previous version (Windows® 95, Windows® 98, and LAN Manager). Note that the sAMAccountName should be less than 20 characters to support of clients and servers from a previous version.

The sAMAccountName must be unique among all security principal objects within a domain.


Groups on Mixed- and Native-Mode Domains

In Windows® 2000, domains can operate in two different modes: mixed or native. Groups behave differently depending on the domain mode.

A mixed-mode domain supports only the following:

  • Global security groups can contain only user accounts.
  • Domain local security groups can contain other global groups and user accounts.
  • Universal security groups cannot be created.
  • Universal distribution groups can be created.
  • Only distribution groups can be nested.
  • No conversion of scope or type is allowed.

A native-mode domain provides all the group features for Windows 2000:

  • Universal groups are available as security groups and distribution groups.
  • Full group nesting is allowed.
  • Groups can be converted freely between security groups and distribution groups. Global groups and domain local groups can be converted to universal groups.

Before creating or converting groups that require native-mode, your application should check the operation mode of the domain.


Detecting the Operation Mode of a Domain

In Windows® 2000, a domain can run in two operation modes: mixed and native. Mixed mode should be used to include domain controllers running Windows NT® 4.0 in a Windows 2000 domain. Mixed mode does not support universal groups or nested groups. If all domain controllers in the domain are running Windows 2000, you can use native mode.

To programmatically detect the operation mode of a Windows 2000 domain, you can read the ntMixedDomain property of the domainDNS object for that domain. A zero value (0) means the domain is in native mode. A value of one (1) means the domain is in mixed mode. You can also use the DsRoleGetPrimaryDomainInformation function to get the operation mode as well as other information about the domain and its state.

To bind to the domainDNS object of the domain that the currently logged on user is a member of, use serverless binding and rootDSE to get the distinguished name for the domain and then use that distinguished name to bind to the domainDNS object that represents that domain. For more information on serverless binding and rootDSE, see the Binding section.


Example Code for Determining the Operation Mode

[C++]

The following code fragment contains a function that reads the ntMixedDomain property of a domain and determines the operation mode:

HRESULT GetDomainMode(IADs *pDomain, BOOL *bIsMixed)
{
HRESULT hr = E_FAIL;
VARIANT var;
if (pDomain)
{
    VariantClear(&var);

    //Get the ntMixedDomain attribute
    LPOLESTR szAttribute = L"ntMixedDomain";
    hr = pDomain->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {

        //Type should be VT_I4.
        if (var.vt==VT_I4)
        {

            //Zero means native mode.
            if (var.lVal == 0)
                *bIsMixed = FALSE;

            //One means mixed mode.
            else if (var.lVal == 1)
                *bIsMixed = TRUE;
            else
                hr=E_FAIL;
        }
    }
    VariantClear(&var);
}
return hr;

}
[Visual Basic]

The script reads the ntMixedDomain property of a domain and determines the operation mode:

'Example: Detects if a domain is in mixed mode or native mode

Dim IADsRootDSE As IADs
Dim IADsDomain As IADs

On Error Resume Next
sComputer = InputBox("This script detects if a Windows 2000 domain is running in mixed or native mode." & vbCrLf & vbCrLf & "Specify the domain name or the name of a domain controller in the domain :")

sPrefix = "LDAP://" & sComputer & "/"

'''''''''''''''''''''''''''''''''''''''
'Bind to a ds server
'''''''''''''''''''''''''''''''''''''''
Set IADsRootDSE = GetObject(sPrefix & "rootDSE")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
   Exit Sub
End If
sDomain = IADsRootDSE.Get("defaultNamingContext")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get method"
   Exit Sub
End If
Set IADsDomain = GetObject(sPrefix & sDomain)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
   Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Get ntMixedDomain property
'''''''''''''''''''''''''''''''''''''''
sMode = IADsDomain.Get("ntMixedDomain")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get ntMixedDomain property"
   Exit Sub
End If
'''''''''''''''''''''''''''''''''''''''
'Get name property
'''''''''''''''''''''''''''''''''''''''
sName = IADsDomain.Get("name")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on Get name property"
   Exit Sub
End If

If (sMode = 0) Then
   sModeString = "Native"
ElseIf (sMode = 1) Then
   sModeString = "Mixed"
Else
   BailOnFailure sMode, "Invalid ntMixedDomain value: " & sMode
   Exit Sub
End If

 strText = "The domain " & sName & " is in " & sModeString & " mode."

MsgBox strText
'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_groups(strText, strName)
    MsgBox strText, vbInformation, "Groups on " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Creating Groups in a Domain

To add a group to Active Directory, you create a group object in the domain container of the domain where you want to place the group. Groups can be created at the root of the domain, within an organizational unit, or within a container.

When you create a group object, you must also set the following attributes to make the object a legal group that Active Directory and the Windows® security system can recognize.

cn
Required. Specify the name of the group object in the directory. This will be the object's relative distinguished name within the container where you create the group.
groupType
Required. Specify an integer that contains the flags that specify the group type and scope using the following combinations:
Group | Flags
Global Security | ADS_GROUP_TYPE_GLOBAL_GROUP | ADS_GROUP_TYPE_SECURITY_ENABLED
Domain Local Security | ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP | ADS_GROUP_TYPE_SECURITY_ENABLED
Universal Security | ADS_GROUP_TYPE_UNIVERSAL_GROUP | ADS_GROUP_TYPE_SECURITY_ENABLED
Global Distribution | ADS_GROUP_TYPE_GLOBAL_GROUP
Domain Local Distribution | ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP
Universal Distribution | ADS_GROUP_TYPE_UNIVERSAL_GROUP

If the group is intended for setting access control on directory objects, you should create Global Security or Universal Security groups.

Note that Universal Security groups can only be created on Windows® 2000 domains running in Native mode. For more information about detecting mixed and native mode, see Detecting the Operation Mode of a Domain.

sAMAccountName
Required. Specify a string that is the name used to support clients and servers from a previous version. Note that the sAMAccountName should be less than 20 characters to support clients of a previous version of Windows NT®.

The sAMAccountName must be unique among all security principal objects within the domain. You should perform a query against the domain to verify that the sAMAccountName is unique within the domain.

Optionally, you can set the member property when creating the group object (using IDirectoryObject::CreateDSObject) to add members (users, groups, or contacts). Or if you use IADsContainer::Create to create the group, you can use IADsGroup::Add to set the member property directly after creation. For more information about adding members to a group, see Adding Members to Groups in a Domain.


Example Code for Creating a Group

[C++]

The following code fragment contains a function that creates a group with only the essential properties explicitly set (cn, sAMAccountType, groupType) and containing no members:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  CreateSimpleGroup()   - Function for creating a basic group

    Parameters

        IDirectoryObject *pDirObject    -   Parent Directory Object for the new group
        LPWSTR pwCommonName             -   Common Name for the new group
        IADs ** ppObjRet -   Pointer to the Pointer which will receive the new Group
        int iGroupType                  -   Bitflags for new group:
                                                                    ADS_GROUP_TYPE_GLOBAL_GROUP,
                                                                    ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP,
                                                                    ADS_GROUP_TYPE_UNIVERSAL_GROUP,
                                                                    ADS_GROUP_TYPE_SECURITY_ENABLED
*/
HRESULT CreateSimpleGroup(IDirectoryObject *pDirObject, LPWSTR pwCommonName,LPWSTR pwSamAcctName,IADs *ppObjRet,int iGroupType)
{
    if(!pDirObject)
        return E_INVALIDARG;

//Check if the group type is Universal Security
//if true, make sure domain is in native mode.
    if(((iGroupType & ADS_GROUP_TYPE_UNIVERSAL_GROUP)==ADS_GROUP_TYPE_UNIVERSAL_GROUP)&&((iGroupType & ADS_GROUP_TYPE_SECURITY_ENABLED)==ADS_GROUP_TYPE_SECURITY_ENABLED))
    {
        //Check whether the domain containing the container is in mixed mode
        hr = CheckDomainModeOfObject(pDirObject, &bIsMixed);

        if (SUCCEEDED(hr))
        {
            if (bIsMixed)
                return E_INVALIDARG;
        }
        else
        {
            return hr;
        }
    }

    //SamAccountName CANNOT be bigger than 20 characters.
    if (wcslen(pwSamAcctName) >20)
    {
        return E_FAIL;
    }

    HRESULT    hr;

    /*
        ADSVALUE is used to specify attribute information for calling IDirectoryObject::CreateDSObject()
        When Creating a new group, the required attributes are : objectClass,sAMAccountName and
        groupType.

        In this function the "objectClass" is set to "group".
        The  "sAMAccountName" is the NT4 name.
        In Windows 2000/Windows NT 4 mixed-mode environment, this attribute
        will be exposed to the Windows NT 4 computers. Therefore, this name must be
        globally unique throughout the network and cannot exceed 20 characters in length.
    */
    ADSVALUE   sAMValue;
    ADSVALUE   classValue;
    ADSVALUE   groupType;

    LPDISPATCH pDisp;
    WCHAR       pwCommonNameFull[1024];

    // Build an Array of ADS_ATTR_INFO structures
    // Note that the sAMAccountName and groupType entries contain
    // a pointer to the respective ADSVALUE structures defined previously
    ADS_ATTR_INFO  attrInfo[] =
    {
       { L"objectClass", ADS_ATTR_UPDATE,
                           ADSTYPE_CASE_IGNORE_STRING, &classValue, 1 },
       {L"sAMAccountName", ADS_ATTR_UPDATE,
                           ADSTYPE_CASE_IGNORE_STRING, &sAMValue, 1},
       {L"groupType", ADS_ATTR_UPDATE,
                           ADSTYPE_CASE_IGNORE_STRING, &groupType, 1}
    };

    // Get the size of the array
    DWORD dwAttrs = sizeof(attrInfo)/sizeof(ADS_ATTR_INFO);

    /*
        For ADSVALUES, the dwType member and
        the data value member (in this case "CaseIgnoreString")
        must be set.
    */

    // To create a group, this parameter must be set to "group".
    classValue.dwType = ADSTYPE_CASE_IGNORE_STRING;
    classValue.CaseIgnoreString = L"group";

    // Set sAMAccountName to the name passed to this function
    sAMValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
    sAMValue.CaseIgnoreString = pwSamAcctName;

    // Set the groupType to the group type passed to this function
    groupType.dwType=ADSTYPE_INTEGER;
    groupType.Integer =  iGroupType;

    //Note that CN is limited to 64 characters.
    // Take the passed commonname and prefix a 'CN=' to conform
    // to the format that  IDirectoryObject::CreateDSObject() requires
    wsprintfW(pwCommonNameFull,L"CN=%s",pwCommonName);

    // Create the new group
    hr = pDirObject->CreateDSObject( pwCommonNameFull,  attrInfo,
                                dwAttrs, &pDisp );
    if (SUCCEEDED(hr))
    {
        // Query the new group for an IADs to be returned from this function
        hr = pDisp->QueryInterface(IID_IADs,(void**) ppObjRet);

        pDisp->Release();
        pDisp = NULL;
    }
    return hr;
}

HRESULT GetDomainMode(IADs *pDomain, BOOL *bIsMixed)
{
HRESULT hr = E_FAIL;
VARIANT var;
if (pDomain)
{
    VariantClear(&var);
    //Get the ntMixedDomain attribute
    LPOLESTR szAttribute = L"ntMixedDomain";
    hr = pDomain->Get(szAttribute,&var);
    if (SUCCEEDED(hr))
    {
        //Type should be VT_I4.
        if (var.vt==VT_I4)
        {
            //Zero means native mode.
            if (var.lVal == 0)
                *bIsMixed = FALSE;
            //One means mixed mode.
            else if (var.lVal == 1)
                *bIsMixed = TRUE;
            else
                hr=E_FAIL;
        }
    }
    VariantClear(&var);
}
return hr;

}

HRESULT CheckDomainModeOfObject(IDirectoryObject *pDirObject, BOOL *bIsMixed)
{
    HRESULT hr = E_FAIL;
    IADs *pDomain = NULL;
    VARIANT VarTest;
    WCHAR *pFound = NULL;
    int iLen;
    WCHAR *pDomainPath = new WCHAR[MAX_PATH*2];

    //Check whether the domain containing the container is in mixed mode
    WCHAR *pVal = NULL;
    pVal = GetDirectoryObjectAttrib(pDirObject,L"canonicalName");

    if (pVal)
    {
        //Parse the canonical name for the DNS name of the domain
        pFound = wcschr(pVal,'/');
        //Bind to the domain using the dns name, get defaultnamingcontext,
        if (pFound)
        {
            iLen = pFound - pVal;
            wcscpy(pDomainPath, L"LDAP://");
            wcsncat(pDomainPath, pVal,iLen);
            wcscat(pDomainPath, L"/rootDSE");
            wprintf(L"DNS Name: %s\n", pDomainPath);
            VariantClear(&VarTest);
            hr = ADsOpenObject(pDomainPath,
                            NULL,
                            NULL,
                            ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                            IID_IADs,
                            (void**)&pDomain);
            if (SUCCEEDED(hr))
            {
                hr = pDomain->Get(L"defaultNamingContext",&VarTest);
                if (SUCCEEDED(hr))
                {
                    wcscpy(pDomainPath, L"LDAP://");
                     wcsncat(pDomainPath, pVal,iLen);
                    wcscat(pDomainPath, L"/");
                    wcscat(pDomainPath, VarTest.bstrVal);
                    VariantClear(&VarTest);
                    if (pDomain)
                        pDomain->Release();
                    if (SUCCEEDED(hr))
                    {
                        hr = ADsOpenObject(pDomainPath,
                                        NULL,
                                        NULL,
                                        ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                                        IID_IADs,
                                        (void**)&pDomain);
                        if (SUCCEEDED(hr))
                        {
                            hr = GetDomainMode(pDomain, bIsMixed);
                        }
                    }
                }
            }
            if (pDomain)
                pDomain->Release();
        }
    }
    return hr;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    GetDirectoryObjectAttrib()      -   Returns the value of the attribute named in pAttrName
                                        from the IDirectoryObject passed
    Parameters

        IDirectoryObject *pDirObject    - Object from which to retrieve an attribute value
        LPWSTR pAttrName                - Name of attribute to retrieve
*/
WCHAR * GetDirectoryObjectAttrib(IDirectoryObject *pDirObject,LPWSTR pAttrName)
{
    HRESULT   hr;
    ADS_ATTR_INFO   *pAttrInfo=NULL;
    DWORD   dwReturn;
    static WCHAR pwReturn[1024];

    pwReturn[0] = 0l;

    hr = pDirObject->GetObjectAttributes( &pAttrName,
                                            1,
                                            &pAttrInfo,
                                            &dwReturn );
    if ( SUCCEEDED(hr) )
    {
        for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
        {
            if ( _wcsicmp(pAttrInfo->pszAttrName,pAttrName) == 0 )
            {
                wcscpy(pwReturn,pAttrInfo->pADsValues->CaseIgnoreString);
                break;
            }
        }
        FreeADsMem( pAttrInfo );
    }
    return pwReturn;
}
[Visual Basic]

The following subroutine creates a group with only the essential properties explicitly set (cn, sAMAccountType, groupType) and containing no members:

'////////////////////////////////////////////////////////////////////////////////////////////////////
'    CreateSimpleGroup()   - Function for creating a basic group
'
'    Parameters
'
'        oDirObject As IDirectoryObject   -   Parent Directory Object for the new group
'        ByVal sCommonName As String      -   Common Name for the new group
'        ByVal sSAMAcctName As String     -   Pointer to the Pointer which will receive the new Group
'        oDirObject As IDirectoryObject      - Parent Directory Object for the new group
'        ByVal sCommonName As String         - Common Name for the new group
'        ByVal sSAMAcctName As String        - Sam Account Name for the new group
'        oDirObjectRet As IDirectoryObject   - New object returned
'        ByVal iGroupType As Long            - Bitflags for new group:
'                                                                    ADS_GROUP_TYPE_GLOBAL_GROUP,
'                                                                    ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP,
'                                                                    ADS_GROUP_TYPE_UNIVERSAL_GROUP,
'                                                                    ADS_GROUP_TYPE_SECURITY_ENABLE
Sub CreateSimpleGroup(oDirObject As IDirectoryObject, ByVal sCommonName As String, ByVal sSAMAcctName As String, oDirObjectRet As IDirectoryObject, ByVal iGroupType As Long)

    If Len(sSAMAcctName) > 20 Then
        MsgBox "SamAccountName CANNOT be bigger than 20 characters"
        Exit Sub
    End If

    Dim sGroupType As String
    Dim oNewObject As IADs
    Dim oIadsContDirObj As IADsContainer
    Set oIadsContDirObj = oDirObject

    ' Get the string value for the group type
    sGroupType = Str(iGroupType)

    ' Get a New group object
    Set oNewObject = oIadsContDirObj.Create("group", "CN=" & sCommonName)

    ' Put the required attributes
    oNewObject.Put "sAMAccountName", sSAMAcctName
    oNewObject.Put "GroupType", iGroupType

    ' Commit the new group
    oNewObject.SetInfo

    ' Print group vitals
    DisplayMessage ">>> Created new GROUP with a groupeType of " & Str(iGroupType)
    PrintIADSObject oNewObject

    Set oDirObjectRet = oNewObject
    Set oNewObject = Nothing
    Set oIadsContDirObj = Nothing

End Sub


Adding Members to Groups in a Domain

You can add users, groups, or contacts to groups. The member property of the group object contains all direct members of the group.

The simplest way to control group membership is to use the IADsGroup::Add and IADsGroup::Remove methods on the IADsGroup object representing the group you want to add or remove members from.

The following properties of the group object control group membership:

member
Use the member property to specify the list of distinguished names for the user, group, and contact objects that are members of the group.

After creating the group object, use the IADsGroup::Add method to add members to the group. Note that IADsGroup::Add uses the ADsPath instead of the distinguished name. To get the ADsPath, use the IADs::get_ADsPath method on the object representing the user, group, or contact you want to add.

memberOf
Optionally, use the memberOf property to specify the list of distinguished names of groups that contain the group as a member.

Note that Active Directory™ maintains this property. When this group's distinguished name is added to the member property of another group, that other group's distinguished name is automatically added to this group's memberOf property.

The IADsGroup methods are simplest for adding and removing members. However, you can also use IDirectoryObject methods to add and remove members. The IDirectoryObject methods may be more efficient if you are adding multiple members at creation time where you can specify the members in the pAttributeEntries parameter of the IDirectoryObject::CreateDSObject method. If you use the IDirectoryObject methods to add members, you must specify distinguished names for the users, groups, or contacts to add.

When using IDirectoryObject, be careful when adding members. The member property is also multi-valued. Therefore, if members already exist in the member property, you must retrieve the array of members using IDirectoryObject::GetObjectAttributes, add the new members to the array, and write the updated array to the member property again using IDirectoryObject::SetObjectAttributes.


Example Code for Adding a Member to a Group

[C++]

The following code fragment contains a function that adds a member to a group:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  AddMemberToGroup()   -       Adds the passed directory object as a member of passed group

    Parameters

        IADsGroup * pGroup       - Group to hold the new IDirectoryObject
        IADs* pIADsNewMember     - Object which will become a member of the group.
                                   Object can be a user, contact, or group.
*/
HRESULT AddMemberToGroup(IADsGroup * pGroup, IADs* pIADsNewMember)
{
    HRESULT hr = E_INVALIDARG;
    if ((!pGroup)||(!pIADsNewMember))
        return hr;

    // Use the IADs::get_ADsPath() member to get the ADsPath
    // Once the ADsPath string is returned, all that is needed
    // to add the new member to the group, is to call the
    // IADsGroup::Add() member, passing in the ADsPath string.
    // Ask the new member for it's AdsPath
    // This is a fully qualified LDAP path to the object to add.
    BSTR bsNewMemberPath;
    hr = pIADsNewMember->get_ADsPath(&bsNewMemberPath);
    if (SUCCEEDED(hr))
    {

        // Use the IADsGroup interface to add the new member.
        // Pass the LDAP path to the
        // new member to the IADsGroup::Add() member
        hr = pGroup->Add(bsNewMemberPath);

        // Free the string returned from IADs::get_ADsPath()
        SysFreeString(bsNewMemberPath);
    }
    return hr;
}
[Visual Basic]

The following subroutine adds a member to a group:

'////////////////////////////////////////////////////////////////////////////////////////////////////
' AddUsersToGroup ()   - Function for adding users to a group
'
'    Parameters
'
'        IDirectoryObject  oDirObject    -   Parent Directory Object for the new group
String array  UsersList           - List of Users to add.

Sub AddUsersToGroup(oDirObject As IDirectoryObject, sUsersList() As String, ByVal iNumUsers As Integer)
    Dim x As Integer

    ' Add the passed Users
    ' go through the list and pull out the ADsPath properties contained therein
    ' use this path to call IADsGroup::Add(), thereby adding the object to the group
    For x = 0 To iNumUsers
        Dim oIADsGroup_NewGroup As IADsGroup
        Set oIADsGroup_NewGroup = oDirObjectRet
        oIADsGroup_NewGroup.Add sUsersList(x)
Set oIADsGroup_NewGroup = Nothing
    Next x

End Sub


Removing Members from Groups in a Domain

You can remove users, groups, or contacts from groups. The member property of the group object contains all direct members of the group.

The simplest way to remove a member from a group is to use the IADsGroup::Remove methods on the IADsGroup object representing the group you want to remove members from.


Example Code for Removing a Member from a Group

The following code fragment contains a function that removes a member from a group:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  RemoveMemberFromGroup()   -       Removes the passed directory object from the membership of passed group

    Parameters

        IADsGroup * pGroup       - Group to remove the member from
        IADs* pIADsNewMember     - Object to remove.
                                   Object can be a user, contact, or group.
*/
HRESULT RemoveMemberFromGroup(IADsGroup * pGroup, IADs* pIADsNewMember)
{
    HRESULT hr = E_INVALIDARG;
    if ((!pGroup)||(!pIADsNewMember))
        return hr;

    // Use the IADs::get_ADsPath() member to get the ADsPath
    // Once the ADsPath string is returned, all that is needed to
    // remove the member from the group, is to call the
    // IADsGroup::Remove() method, passing in the ADsPath string.
    // Ask the member for it's AdsPath
    // This is a fully qualified LDAP path to the object to remove.
    BSTR bsNewMemberPath;
    hr = pIADsNewMember->get_ADsPath(&bsNewMemberPath);
    if (SUCCEEDED(hr))
    {

        // Use the IADsGroup interface to remove the member.
        // Pass the LDAP path to the
        // member to the IADsGroup::Remove() method
        hr = pGroup->Remove(bsNewMemberPath);

        // Free the string returned from IADs::get_ADsPath()
        SysFreeString(bsNewMemberPath);
    }
    return hr;
}


Nesting a Group in Another Group

Adding a group as a member of a group is called nesting. For distribution groups, nesting is supported in both mixed-mode and native-mode. For security groups, nesting is supported only on domains running in native-mode.

To nest a group in another group, you use the same techniques described in Adding members to groups in a domain.

Note that depending of the scope of the group, the group can contain only specific types of groups from certain domains. For more information about the effect of group scope on members, see Scope of groups.

Your nesting options depend on whether the domain is in mixed-mode or native-mode.


Nesting in Native Mode

The following list describes what can be contained in a group that exists in a native-mode domain. This same list also applies to distribution groups in mixed-mode domains. The scope of the group determines these containment rules. Universal groups can contain user accounts, universal groups, and global groups from any domain.

Global groups can contain user accounts from the same domain, and global groups from the same domain.

Domain local groups can contain user accounts, universal groups, and global groups, all from any domain. They can also contain other domain local groups from within the same domain.


Nesting in Mixed Mode

Security groups in a mixed-mode domain can contain only the following:

  • Global groups can contain only user accounts.
  • Domain local groups can contain other global groups and user accounts.
  • Universal groups cannot be created in mixed-mode domains because the universal scope is supported only in Windows® 2000 native-mode domains.

Note that distribution groups can be nested according to the nesting rules for native-mode.


Common Errors

The following table contains a list of common errors that can occur based on the scope of the group being nested.

HRESULT | Meaning
0x8007001F | General failure. This error occurs if you attempt to add 1) a Domain Local group to a Global or Universal group, 2) a Universal group to a Global group, or 3) a Domain Local Domain Local groups can only be added as members to other Domain Local groups in the same domain. Universal groups can be added to Universal and Domain Local groups but not Global groups.
0x8007202F | ERROR_DS_CONSTRAINT_VIOLATION. This error occurs if you attempt to nest a security group (that is, add a group as a member of another group) in another group in a domain running in mixed-mode. Security groups cannot be nested in mixed-mode; security groups can only be nested in native-mode.


Determining a User's or Group's Membership in a Group

The IADsGroup interface has an IADsGroup::IsMember method. This method returns TRUE if the specified object is a direct member of the group, that is, the group's member property contains the specified object.

Note that a group can contain other groups (nesting). The IADsGroup::IsMember method does not recursively check the member properties of groups in its member property, groups within those groups, and so on. To recursively check if an object is a member of a group, you must enumerate the groups in the member property, check the members of those groups to see if the object is a member, and if those groups contain other groups, check their members, and so on.


Example Code for Checking for Membership in a Group

The following code fragment contains a function that checks for absolute membership of an object by recursively checking whether an object is a member of a group or any groups nested within that group:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  RecursiveIsMember()                       - Recursively scans the members of passed IADsGroup ptr
                                                and any groups belonging to the passed ptr- for membership
                                                of Passed group
                                                Will return a TRUE if the member is found in the passed group,
                                                Or if the passed member is a member of any group which is a member
                                                of the passed group
    Parameters

        IADsGroup *     pADsGroup       - Group from which to check members
        LPWSTR          pwszMember      - LDAP path for object to check membership
        BOOL            bVerbose        - IF TRUE, will output verbose information on the scanning

    OPTIONAL Parameters

       LPOLESTR  pwszUser           - User Name and Password, if the parameters are NOT passed,
       LPOLESTER pwszPassword       - Binding will use ADsGetObject, if the parameters
                                    - Are specified, will use ADsOpenObject, passing user name and password
*/

BOOL RecursiveIsMember(IADsGroup * pADsGroup,LPWSTR pwszMemberGUID,LPWSTR pwszMemberPath,
                                             BOOL bVerbose, LPOLESTR  pwszUser, LPOLESTR pwszPassword)
{
    HRESULT         hr                = S_OK;     // COM Result Code
    IADsMembers *   pADsMembers       = NULL;     // Ptr to Members of the IADsGroup
    BOOL            fContinue         = TRUE;     // Looping Variable
    IEnumVARIANT *  pEnumVariant      = NULL;     // Ptr to the Enum variant
    IUnknown *      pUnknown          = NULL;     // IUnknown for getting the ENUM initially
    VARIANT         VariantArray[FETCH_NUM];      // Variant array for temp holding returned data
    ULONG           ulElementsFetched = NULL;     // Number of elements fetched
    BSTR            bsGroupPath       = NULL;
    BOOL            bRet              = FALSE;

    // Get the path of the object passed in
    hr = pADsGroup->get_ADsPath(&bsGroupPath);

    if (!SUCCEEDED(hr))
        return hr;

    if (bVerbose)
    {
        WCHAR pwszOutput[2048];
        wsprintf(pwszOutput,L"Checking the Group:\n\n%s\n\n for the member:\n\n%s\n\n",bsGroupPath,pwszMemberPath);
        PrintBanner(pwszOutput);
    }

    // Get an interface pointer to the IADsCollection of members
    hr = pADsGroup->Members(&pADsMembers);

    if (SUCCEEDED(hr))
    {
        // Ask the IADsCollection of members for a new ENUM Interface
        // Note the enum comes back as an IUnknown *
        hr = pADsMembers->get__NewEnum(&pUnknown);

        if (SUCCEEDED(hr))
        {
            // QI the IUnknown * for an IEnumVARIANT interface
            hr = pUnknown->QueryInterface(IID_IEnumVARIANT, (void **)&pEnumVariant);

            if (SUCCEEDED(hr))
            {
                // While have not hit errors or end of data....
                while (fContinue)
                {
                   ulElementsFetched = 0;
                    // Get a "batch" number of group members- number of rows specified by FETCH_NUM
                    hr = ADsEnumerateNext(pEnumVariant, FETCH_NUM, VariantArray, &ulElementsFetched);

                    if (ulElementsFetched )
                    {
                        // Loop through the current batch- printing the path
                        // for each member
                        for (ULONG i = 0; i < ulElementsFetched; i++ )
                        {
                            IDispatch * pDispatch         = NULL; // ptr for holding dispath of element
                            BSTR        bstrCurrentPath   = NULL; // Holds path of object
                            BSTR        bstrGuidCurrent   = NULL; // Holds path of object
                            IDirectoryObject * pIDOCurrent = NULL;// Holds the current object

                            // Get the dispatch ptr for the variant
                            pDispatch = VariantArray[i].pdispVal;
                            assert(HAS_BIT_STYLE(VariantArray[i].vt,VT_DISPATCH));

                            // Get the IADs interface for the "member" of this group
                            hr = pDispatch->QueryInterface(IID_IDirectoryObject,
                                                           (VOID **) &pIDOCurrent ) ;

                            if (SUCCEEDED(hr))
                            {
                                // Retrieve the GUID for the current object
                                hr = GetObjectGuid(pIDOCurrent,bstrGuidCurrent);

                                if (FAILED(hr))
                                    return hr;

                                IADs * pIADsCurrent = NULL;

                                // Retrieve the IADs Interface for the current object
                                hr = pIDOCurrent->QueryInterface(IID_IADs,(void**)&pIADsCurrent);
                                if (FAILED(hr))
                                    return hr;

                                // Get the ADsPath property for this member
                                hr = pIADsCurrent->get_ADsPath(&bstrCurrentPath);

                                if (SUCCEEDED(hr))
                                {
                                    if (bVerbose)
                                        wprintf(L"Comparing:\n\n%s\nWITH:\n%s\n\n",bstrGuidCurrent,pwszMemberGUID);

                                    // Is the "member of this group Equal to passed?
                                    if (_wcsicmp(bstrGuidCurrent,pwszMemberGUID)==0)
                                    {
                                        if (bVerbose)
                                            wprintf(L"!!!!!Object:\n\n%s\n\nIs a member of\n\n%s\n\n",pwszMemberPath,bstrGuidCurrent);

                                        bRet = TRUE;
                                        break;
                                    }
                                    else // Otherwise, we should bind to this and see if it is a group
                                    {    // If is it a group then the QI to IADsGroup will succeed

                                        IADsGroup * pIADsGroupAsMember = NULL;

                                        if (pwszUser)
                                            hr = ADsOpenObject( bstrCurrentPath,
                                                                pwszUser,
                                                                pwszPassword,
                                                                ADS_SECURE_AUTHENTICATION,
                                                                IID_IADsGroup,
                                                                (void**) &pIADsGroupAsMember);
                                        else
                                            hr = ADsGetObject( bstrCurrentPath, IID_IADsGroup,(void **)&pIADsGroupAsMember);

                                        // If we DID bind, then this IS a group
                                        if (SUCCEEDED(hr))
                                        {
                                            // Recursively call ourselves to check THIS group
                                            BOOL bRetRecurse;
                                            bRetRecurse = RecursiveIsMember(pIADsGroupAsMember,pwszMemberGUID,pwszMemberPath,bVerbose,pwszUser ,pwszPassword );

                                            if (bRetRecurse)
                                            {
                                                bRet = TRUE;
                                                break;
                                            }
                                            pIADsGroupAsMember->Release();
                                            pIADsGroupAsMember = NULL;
                                        }
                                    }
                                    SysFreeString(bstrCurrentPath);
                                    bstrCurrentPath = NULL;

                                    SysFreeString(bstrGuidCurrent);
                                    bstrGuidCurrent = NULL;
                                }
                                // Release
                                pIDOCurrent->Release();
                                pIDOCurrent = NULL;
                                if (pIADsCurrent)
                                {
                                    pIADsCurrent->Release();
                                    pIADsCurrent = NULL;
                                }
                            }
                         }
                        // Clear the variant array
                        memset(VariantArray, 0, sizeof(VARIANT)*FETCH_NUM);
                    }
                    else
                        fContinue = FALSE;
                }
                pEnumVariant->Release();
                pEnumVariant = NULL;
            }
            pUnknown->Release();
            pUnknown = NULL;
        }
        pADsMembers ->Release();
        pADsMembers  = NULL;
    }

    // Free the group path if it was retrieved.
    if (bsGroupPath)
    {
        SysFreeString(bsGroupPath);
        bsGroupPath = NULL;
    }
    return bRet;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
/*  GetObjectGuid()    - Retrieves the GUID in String form from the passed Directory Object
                         Returns S_OK on success

    Parameters

        IDirectoryObject *  pDO     -   Directory Object from where GUID will be retrieved.
        BSTR             &  bsGuid  -   Returned GUID
*/

HRESULT GetObjectGuid(IDirectoryObject * pDO,BSTR &bsGuid)
{

    GUID *pObjectGUID   = NULL;
    PADS_ATTR_INFO      pAttributeEntries;
    LPWSTR              pAttributeName = L"objectGUID";  //Get the GUID for the object
    DWORD               dwAttributesReturned = 0;
    HRESULT             hr;
    hr = pDO->GetObjectAttributes(  &pAttributeName, //objectGUID
                                    1, //Only objectGUID
                                    &pAttributeEntries, // Returned attributes
                                    &dwAttributesReturned //Number of attributes returned
                                    );

    if (SUCCEEDED(hr) && dwAttributesReturned>0)
    {
        //Make sure that we got the right type--objectGUID is ADSTYPE_OCTET_STRING
        if (pAttributeEntries->dwADsType == ADSTYPE_OCTET_STRING)
        {
            //Get COM converted string version of the GUID
            //lpvalue should be LPBYTE. Should be able to cast it to ptr to GUID.
            pObjectGUID = (GUID*)(pAttributeEntries->pADsValues[0].OctetString.lpValue);

            //OLE str to fit a GUID

            LPOLESTR szGUID = new WCHAR [64];
            szGUID[0]=NULL;
            //Convert GUID to string.
            ::StringFromGUID2(*pObjectGUID, szGUID, 39);
            bsGuid = SysAllocString(szGUID);

            delete [] szGUID;
         }
    }

    return hr;

}
////////////////////////////////////////////////////////////////////////////////////////////////////
/*  PrintBanner()   -       Prints a banner on the screen

    Parameters

                LPOLESTR pwszBanner   - String to print
*/
void PrintBanner(LPOLESTR pwszBanner)
{
    _putws(L"");
    _putws(L"////////////////////////////////////////////////////");
    wprintf(L"\t");
    _putws(pwszBanner);
    _putws(L"////////////////////////////////////////////////////\n");
}


Enumerating Groups

This section contains the following information:

  • Enumerating Groups in a Domain
  • Enumerating Groups by Scope or Type in a Domain
  • Enumerating Members in a Group
  • Enumerating Groups That Contain Many Members


Enumerating Groups in a Domain

Unlike Windows NT® 4.0 domains, Windows® 2000 groups can be placed in any container or organizational unit in a domain as well as the root of the domain. This means that groups can be in numerous locations in the directory hierarchy. Therefore, you have two choices for enumerating groups:

  1. Enumerate the groups directly contained in a container, OU, or at the root of the domain.

    Explicitly bind to the container object containing the groups you are interested in enumerating, set a filter containing "groups" as the class using the IADsContainer::put_Filter property method, and use the IADsContainer::get__NewEnum method to enumerate the group objects.

    Note this technique is useful if you want to enumerate groups that are directly contained in a container or OU object. If the container contains other containers that can potentially contain other groups, you need to bind to those containers and recursively enumerate the groups on those containers. If you do not need to manipulate the group objects and only need to read specific properties, you should use the deep search described in option 2.

  2. Because enumeration returns pointers to ADSI COM objects representing each group object, you can call QueryInterface to get IADs, IADsGroup, and IADsPropertyList interface pointers to the group object. This means you can get interface pointers to each enumerated group object in a container without having to explicitly bind to each group object. If you wanted to perform operations on all the groups directly within a container, enumeration saves you from having to bind to each group in order to call IADs or IADsGroup methods. If you only want to retrieve specific properties from groups, you should use IDirectorySearch as described in option 3.
  3. Perform a deep search for objectCategory=group to find all groups in a tree.

    First, bind to the container object where you want to begin the search. For example, if you wanted to find all groups in a domain, you would bind to root of the domain; if you wanted to find all groups in the forest, you would bind to global catalog and search from the root of the GC.

    Then use IDirectorySearch to query using a search filter containing (objectCategory=group) and search preference of ADS_SCOPE_SUBTREE.

    Note that you can perform a search with a search preference of ADS_SCOPE_ONELEVEL to limit the search to the direct contents of the container object that you bound to.

    IDirectorySearch retrieves only the values of specific properties from groups. If you only want to retrieve values, you should use IDirectorySearch. If you want to manipulate the group objects returned from a search (that is, you want to use IADs or IADsGroup methods), you must explicitly bind to them (to do this, specify distinguishedName as one of the properties to return from the search and use the returned distinguished names to bind to each group returned in the search).

    Note that only specific properties are retrieved. You cannot retrieve all attributes without explicitly specifying every possible attribute of the group class.


Enumerating Groups by Scope or Type in a Domain

In Windows® 2000 domains, there is single class called group for all group scopes (Domain Local, Global, Universal) and types (security, distribution). The groupType property of the group object specifies the group type and scope.

To use type or scope to enumerate or search for groups on Windows 2000 domains, you must use a filter that contains a matching rule for the groupType property.

Matching rules have the following syntax:

attibutename:ruleOID:=value

where attributename is the ldapDisplayName of the attribute, ruleOID is the OID for the matching rule, and value is the value you want to use for comparison.

Active Directory supports the two matching rules shown in the following table.

Matching Rule OID | Description
1.2.840.113556.1.4.803 | LDAP_MATCHING_RULE_BIT_AND The matching rule is true only if all bits from the property match the value. This rule is like the bit-wise AND operator.
1.2.840.113556.1.4.804 | LDAP_MATCHING_RULE_BIT_OR The matching rule is true if any bits from the property match the value. This rule is like the bit-wise OR operator.


Example Query Strings

The following query string searches for security groups:

(&(objectCategory=group)(groupType:1.2.840.113556.1.4.804: = ADS_GROUP_TYPE_SECURITY_ENABLED) )

The following query string searches for Universal distribution groups (that is, Universal groups without ADS_GROUP_TYPE_SECURITY_ENABLED flag):

(&(objectCategory=group)((&(groupType:1.2.840.113556.1.4.804:=ADS_GROUP_TYPE_UNIVERSAL_GROUP)(!(groupType:1.2.840.113556.1.4.803:=ADS_GROUP_TYPE_SECURITY_ENABLED|ADS_GROUP_TYPE_UNIVERSAL_GROUP)))) )

The following query string searches for all Universal groups — this will include Universal groups that are of type security as well as distribution:

(&(objectCategory=group)(groupType:1.2.840.113556.1.4.804: = ADS_GROUP_TYPE_UNIVERSAL_GROUP) )

The following query string searches for Universal security groups:

(&(objectCategory=group)(groupType:1.2.840.113556.1.4.803: = ADS_GROUP_TYPE_UNIVERSAL_GROUP|ADS_GROUP_TYPE_SECURITY_ENABLED) )


Example Code for Building a Query String to Search for Groups by Type/Scope

[C++]

The following code fragment contains a function that creates a query string that uses type or scope for enumerating or searching for groups:

////////////////////
// Enum Definitions
//This first enum should be defined for Beta 3 and later.
enum
    {    ADS_GROUP_TYPE_GLOBAL_GROUP    = 0x2,
    ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP    = 0x4,
    ADS_GROUP_TYPE_UNIVERSAL_GROUP    = 0x8,
    ADS_GROUP_TYPE_SECURITY_ENABLED    = 0x80000000
    };

// (used for telling BuildGroupTypeQueryString() how to build the string)
enum ELDAPMatchingRule
{
    lmrAND,
    lmrOR
};
////////////////////////////////////////////////////////////////////////////////////////////////////
/*  BuildGroupTypeQueryString()   - Builds a Query String for searching on groupType

    Parameters

        ELDAPMatchingRule elmrRule      - Specifies whether the passed bitmask is ANDed or ORed
        DWORD dwGroupTypeBits           - Bitflags for groupType

*/
WCHAR * BuildGroupTypeQueryString(ELDAPMatchingRule elmrRule, DWORD dwGroupTypeBits)
{
    static WCHAR wszRet [255];
    wszRet[0] = 0l;
    /*
        The LDAP_MATCHING RULE is defined as follows:

        LDAP_MATCHING_RULE_BIT_OR 1.2.840.113556.1.4.804
        LDAP_MATCHING_RULE_BIT_AND 1.2.840.113556.1.4.803

        These strings are used to describe the bits in dwGroupTypeBits.
        Specifying an "and" (using LDAP_MATCHING_RULE_BIT_AND) causes
        the query to mean a bit-wise "and" for the bits in dwGroupTypeBits.
        Alternatively, specifying LDAP_MATCHING_RULE_BIT_OR denotes a bit-wise
        "or" of the flags present in dwGroupTypeBits
    */
    switch (elmrRule)
    {
        case lmrAND:
        wsprintf(wszRet,L"(groupType:1.2.840.113556.1.4.803:=%d)",dwGroupTypeBits);
        break;
        case lmrOR:
        wsprintf(wszRet,L"(groupType:1.2.840.113556.1.4.804:=%d)",dwGroupTypeBits);
        break;
    }
    OutputDebugStringW(wszRet);
    OutputDebugStringW(L"\r\n");
    return wszRet;
}
[Visual Basic]

When using Visual Basic®, use the ADO SQL syntax for specifying Group Type.

Definitions for bit values:

Public Const ADS_GROUP_TYPE_GLOBAL_GROUP = &H2
Public Const ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP = &H4
Public Const ADS_GROUP_TYPE_UNIVERSAL_GROUP = &H8
Public Const ADS_GROUP_TYPE_SECURITY_ENABLED = &H80000000

Query Strings:

ALL GLOBAL GROUPS

"GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP) & " or " & "GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED)

ALL DOMAIN LOCAL GROUPS (With and without Security)

"GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) & " or " & "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED)

ALL SECURITY ENABLED GROUPS

"GroupType=" & Str(ADS_GROUP_TYPE_SECURITY_ENABLED)

The following code fragment searches for groups based on type using the BuildGroupTypeQueryString subroutine:

'************************************
'  QueryAndOutputGroups()   -           Performs several sample searches using ADO
'
'
'    Parameters
'
'        oDirObjectRoot As IDirectoryObject - The root from where searches will be performed
'
Sub QueryAndOutputGroups(oDirObjectRoot As IDirectoryObject)

    '*******************************
    ' Search for all the groups
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL GROUPS>>>>>>>>>>>>>>>>>" 'cn = 'UG*' objectCategory
    SimpleDirectorySearch oDirObjectRoot, "objectCategory = 'group'", ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the GLOBAL groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the GLOBAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL GLOBAL Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the DOMAIN LOCAL  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the DOMAIN LOCAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '        #ifdef USE_UNIVERSAL_GROUPS

    '*****************************************
    ' Search for all the UNIVERSAL  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the UNIVERSAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Universal Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_UNIVERSAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_UNIVERSAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE
    '        #End If

    '*****************************************
    ' Search for all the Security Enabled  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all Security Enabled groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_SECURITY_ENABLED) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_SECURITY_ENABLED Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the Security Enabled LOCAL groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all Security Enabled LOCAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Domain Local Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or _
                            ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the DOMAIN LOCAL groups whose
    ' name begins with 'UGExercise'
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all DOMAIN LOCAL  groups that begin with the text 'UGExercise'"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups Beginnig with 'UGExercise' >>>>>>>>>>>>>>>>>"

    SimpleDirectorySearch oDirObjectRoot, "cn='UGExercise*' and (GroupType=" & _
                            Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) & " or GroupType=" & _
                            Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED) & _
                            ")", ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for ALL the groups whose
    ' name begins with 'UGExercise'
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of ALL groups that begin with the text 'UGExercise '"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Groups Beginning with 'UGExercise' >>>>>>>>>>>>>>>>>"

    SimpleDirectorySearch oDirObjectRoot, " cn='UGExercise*' and objectCategory='group'", ADS_SCOPE_SUBTREE

End Sub

'////////////////////////////////////////////////////////////////////////////////////////////////////
'   SimpleDirectorySearch()   - Uses ADO to search the directory
'                               Results are displayed in the list box
'    Parameters
'
'        oDirObjectRoot As IDirectoryObject  - Root of the search
'        ByVal sSearchFilter As String       - LDAP Search filter
'        ByVal lScope As Integer             - Scope for Searching possible values are:
'                                                ADS_SCOPE_BASE
'                                                ADS_SCOPE_ONELEVEL
'                                                ADS_SCOPE_SUBTREE
'
'        (See IDirectorySearch and ADS_SCOPEENUM in the docs for more info)
'
Sub SimpleDirectorySearch(oDirObjectRoot As IDirectoryObject, ByVal sSearchFilter As String, ByVal lScope As Integer)

    Dim iIndex As Integer
    iIndex = 0
    Dim v, j, i

    Dim con As New Connection, rs As New Recordset
    Dim Com As New Command
    Dim oIADs As IADs
    Dim sAdsPathRoot As String

    ' Get the LDAP path to the passed in object
    sAdsPathRoot = GetAdsPath(oDirObjectRoot)

    'Open a Connection object
    con.Provider = "ADsDSOObject"

    '---------------------------------------------------------------------
    ' If you want to be authenticated other than current logged on user
    ' use connection properties of User ID and Password
    '---------------------------------------------------------------------
    ' con.Properties("User ID") = "Administrator"
    ' con.Properties("Password") = ""

    ' Open the connection
    con.Open "Active Directory Provider"

    ' Create a command object on this connection
    Set Com.ActiveConnection = con

    ' set the query string using SQL Dialect
    Com.CommandText = "select name,AdsPath from '" & sAdsPathRoot & "' where " & sSearchFilter & " ORDER BY NAME"

    ' Tell the user what the search filter is
    DisplayMessage "Search Filter = " & Com.CommandText

    '---------------------------------------------------
    ' Or you can use LDAP Dialect, for example,
    '---------------------------------------------------
    ' Ex Com.CommandText = "<LDAP://arcadiabaysvr1/dc=arcadiabay,DC=com>;(objectClass=*);name"
    ' For LDAP Dialect, the valid search scope are base, oneLevel and subtree
    ' Com.CommandText = "<" & adDomainPath & ">;(objectClass=*);name;subtree"
    ' For LDAP Dialect (<LDAP:...>), there is no way to specify sort order in the string,
    ' However, you can use this SORT ON property to specify sort order.
    ' for SQL Dialect you can use ORDER BY in the SQL Statement
    ' Ex. Com.Properties("Sort On") = "Name"

    'Set the preferences for Search
    Com.Properties("Page Size") = 1000
    Com.Properties("Timeout") = 30 'seconds
    Com.Properties("searchscope") = lScope
    Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
    Com.Properties("Cache Results") = False ' do not cache the result, it results in less memory requirements

    'Execute the query
    Set rs = Com.Execute

    ' Tell the user how many rows
    DisplayMessage "Returned " & Str(rs.RecordCount) & " rows"

    ' Navigate the record set
    If Not rs.EOF Then
        rs.MoveFirst
    End If

    On Error Resume Next
    While Not rs.EOF
        ' Display the LDAP path for the row
        DisplayMessage rs.Fields("AdsPath")
        rs.MoveNext
    Wend

End Sub


Enumerating Members in a Group

The members of a group are stored in a multi-valued property called member. For groups with a small to medium-sized membership, you can use the IADsGroup::Members method to get a pointer to an IADsMembers object containing the list of all members and then use the IADsMembers::get__NewEnum to get an enumerator object that you can use to enumerate the members.

For the group membership is potentially large, you should use ranging to retrieve users one range at a time. For information on using ranging to enumerate members, see Enumerating Groups That Contain Many Members.


Example Code for Displaying Members of a Group

[C++]

The following code fragment contains a function that displays the members of a group using IADsGroup and IADsMembers:

//////////////////////////////
/*  PrintGroupObjectMembers()
- Prints the members of the group that the pADsGroup passes.
    Parameters
        IADsGroup * pADsGroup       - Group from which to list members
*/
HRESULT PrintGroupObjectMembers(IADsGroup * pADsGroup)
{
    HRESULT         hr                = S_OK;     // COM Result Code
    IADsMembers *   pADsMembers       = NULL;     // Ptr to Members of the IADsGroup
    BOOL            fContinue         = TRUE;     // Looping Variable
    IEnumVARIANT *  pEnumVariant      = NULL;     // Ptr to the Enum variant
    IUnknown *      pUnknown          = NULL;     // IUnknown for getting the ENUM initially
    VARIANT         VariantArray[FETCH_NUM];      // Variant array for temp holding returned data
    ULONG           ulElementsFetched = NULL;     // Number of elements fetched

    // Get an interface pointer to the IADsCollection of members
    hr = pADsGroup->Members(&pADsMembers);

    if (SUCCEEDED(hr))
    {

        // Ask the IADsCollection of members for a new ENUM Interface
        // Note the enum comes back as an IUnknown *
        hr = pADsMembers->get__NewEnum(&pUnknown);

        if (SUCCEEDED(hr))
        {

            // QI the IUnknown * for a IEnumVARIANT interface
            hr = pUnknown->QueryInterface(IID_IEnumVARIANT, (void **)&pEnumVariant);

            if (SUCCEEDED(hr))
            {

                // While have not hit errors or end of data....
                while (fContinue)
                {
                   ulElementsFetched = 0;

                    // Get a "batch" number of group members- number of rows that FETCH_NUM specifies
                    hr = ADsEnumerateNext(pEnumVariant, FETCH_NUM, VariantArray, &ulElementsFetched);

                    if (ulElementsFetched )//SUCCEEDED(hr) && hr != S_FALSE)
                    {

                        // Loop through the current batch, printing
                        // the path for each member
                        for (ULONG i = 0; i < ulElementsFetched; i++ )
                        {
                            IDispatch * pDispatch         = NULL;
                            // ptr for holding dispath of element
                            IADs      * pIADsGroupMember  = NULL;
                            // IADs ptr to group member
                            BSTR        bstrPath          = NULL;
                            // Holds path of object

                            // Get the dispatch ptr for the variant
                            pDispatch = VariantArray[i].pdispVal;
                            assert(HAS_BIT_STYLE(VariantArray[i].vt,VT_DISPATCH));

                            // Get the IADs interface for the "member" of this group
                            hr = pDispatch->QueryInterface(IID_IADs,
                                                           (VOID **) &pIADsGroupMember) ;

                            if (SUCCEEDED(hr))
                            {

                            // Get the ADsPath property for
                            //this member
                                hr = pIADsGroupMember->get_ADsPath(&bstrPath) ;

                                if (SUCCEEDED(hr))
                                {

                                    // Print the ADsPath of the
                                    //group member
                                    wprintf(L"\tMember Object: %ws\n", bstrPath);
                                    SysFreeString(bstrPath);
                                }
                                pIADsGroupMember->Release();
                                pIADsGroupMember   = NULL;
                            }
                         }

                        // Clear the variant array
                        memset(VariantArray, 0, sizeof(VARIANT)*FETCH_NUM);
                    }
                    else
                        fContinue = FALSE;
                }
                pEnumVariant->Release();
                pEnumVariant = NULL;
            }
            pUnknown->Release();
            pUnknown = NULL;
        }
        pADsMembers ->Release();
        pADsMembers  = NULL;
    }

    // If everything WENT ok, all the data
    // was printed,  and an S_FALSE (indicating
    // no more data) was received. If that is the case,
    // you REALLY want to return an S_OK
    if (hr == S_FALSE)
        hr = S_OK;

    return hr;
}
[Visual Basic]

The following code fragment contains a function that displays the members of a group:

'////////////////////////////////////////////////////////////////////////////////////////////////////
'    PrintGroupObjectMembers()    - Prints the members of passed IADsGroup ptr
'
'    Parameters
'
'        oADsGroup As IADsGroup       - Group from which to list members
'
'HRESULT PrintGroupObjectMembers(IADsGroup * pADsGroup)
Sub PrintGroupObjectMembers(oADsGroup As IADsGroup)

    Dim child As IADs
    For Each child In oADsGroup.Members
        PrintIADSObject child
    Next child

End Sub


Enumerating Groups That Contain Many Members

The members of a group are stored in a multi-valued property called member. The group membership may potentially contain a large number of values. This can be inconvenient or even impossible when the number of values in a multi-valued attribute becomes very large.

Active Directory supports incremental retrievals for this purpose. You may specify the range specifiers for multi-valued properties.

Range specifiers for a property take the following form:

attributename;Range=range

where attributename is the ldapDisplayName of the attribute and range is the range of values that you want to retrieve from the property. The * wildcard can be used to specify "to the end of the list" and 0 is used to specify the first entry in the list.

The following table contains examples of range specifiers.

Example | Meaning
Member;Range=0-500 | Retrieve entries from 0 to 500 inclusively. The maximum potential entries are 501 values.
Member;Range=501-* | Retrieve the 502nd entry and beyond.
Member;Range=2-3 | Retrieve the 3rd and 4th entries.

IDirectorySearch, IDirectoryObject, and ADO support range retrievals.


ADO SQL Dialect

In ADO using SQL dialect, you must use single quotes around the attribute and range specifier.

For example:

Command.Text = "select Name, 'member;Range=0-50' from 'LDAP://CN=Group1,DC=ArcadiaBay,DC=Com'  where objectCategory='group'"


ADO LDAP Dialect

In ADO using LDAP dialect, the attribute and its range specifier does not use quotes.

For example:

Command.Text = <LDAP://CN=NewGroup,DC=ArcadiaBay,DC=Com>;(objectCategory=group);
name,member;Range=51-*;base"


IDirectorySearch and IDirectoryObject

When you use IDirectorySearch or IDirectoryObject, the attribute and range specifier replace the name of the attribute in the pAttributesName array that specifies the names of the attributes to return from the IDirectorySearch::ExecuteSearch and IDirectoryObject::GetObjectAttributes methods.

For example:

LPWSTR pszAttrs[2];

pszAttrs[0] = L"Name";
pszAttrs[1] = L"member;Range=0-100";


Example Code for Using Ranging to Retrieve Members of a Group

[C++]

The following code fragment contains a function that uses ranging to retrieve the members of a group using IDirectoryObject:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  PrintAttributeWithRanging()    - Uses IDirectoryObject to read an attribute with ADSI ranging

    Parameters

        IDirectoryObject * pDirObject   - Object from which the attribute will be read
        LPOLESTR pwszAttribute          - Name of the attribute
        int iNumToDisplayAtATime        - Number of Attributes to retrieve at a time

*/
HRESULT PrintAttributeWithRanging(IDirectoryObject * pDirObject,LPOLESTR pwszAttribute, int iNumToDisplayAtATime)
{
    HRESULT hr = S_OK;

    ADS_ATTR_INFO   *pAttrInfo=NULL;
    // Returned Data
    DWORD           dwReturn;
    // number of returned elements
    WCHAR           pwszRangeAttrib[512];
    // String for building Range
    LPWSTR          pAttrNames[]={pwszRangeAttrib};
    // Array for passing attribute array
    DWORD           dwNumAttr =sizeof(pAttrNames)/sizeof(LPWSTR);
    // Number of attributes requested

    // Do a series of range searches, grabbing
    // iNumToDisplayAtATime at a time.
    int iRange = 0;
    DWORD dAttribsGotten = 0;

    while (SUCCEEDED(hr))
    {
        int     iAttribsGottenLastSrch =0;

        // Build the Range String : ex: L"Member;Range=0-5"
        swprintf(pwszRangeAttrib,L"%s;Range=%d-%d",pwszAttribute,iRange,iRange+iNumToDisplayAtATime-1);

        // Get the object Attributes
        hr = pDirObject->GetObjectAttributes(pAttrNames,
                                                dwNumAttr,
                                                &pAttrInfo,
                                                &dwReturn );
        if(SUCCEEDED(hr))
        {

            // Loop through all the rows returned
            for (DWORD x = 0; x< dwReturn; x++)
            {

                // Switch on TYPE returned
                switch ((pAttrInfo+x)->dwADsType)
                {
                    case ADSTYPE_DN_STRING:
                    case ADSTYPE_CASE_EXACT_STRING:
                    case ADSTYPE_CASE_IGNORE_STRING:
                    case ADSTYPE_PRINTABLE_STRING:
                    {

                        // If the returned value is multi-valued
                        //(which it is when using ranging),
                        // then loop through all the values in
                        //the multi-value
                        for (DWORD dwVal = 0 ; dwVal < (pAttrInfo+x)->dwNumValues; dwVal++)
                        {

                            // Print the values in the multi-valued
                            //return data
                            wprintf(L"%d: %s = %s\n",dAttribsGotten,(pAttrInfo+x)->pszAttrName,((pAttrInfo+x)->pADsValues+dwVal)->CaseIgnoreString);

                            // Increment some counters for checking
                            // whether LESS data was
                            // returned than was asked for (which
                            // means you are done)
                            dAttribsGotten++;
                            iAttribsGottenLastSrch++;
                        }
                        break;
                    }
                    default:
                        wprintf(L"%d: %s = !!UnHandled Type!!\n",dAttribsGotten,(pAttrInfo+x)->pszAttrName);

                    FreeADsMem( pAttrInfo );
                    pAttrInfo = NULL;
                }
            }
        }

        if (FAILED(hr) || hr ==S_ADS_NOMORE_ROWS || iAttribsGottenLastSrch== 0 ||
                                                    iAttribsGottenLastSrch< iNumToDisplayAtATime  )
            break;

        // Increment the range counter, so the next string that is
        // built will get the next range
        iRange += iNumToDisplayAtATime;
   }

    // Use FreeADsMem for all memory obtained from ADSI call
    return S_OK;
}
[Visual Basic]

The following code fragment contains a subroutine that uses ranging to retrieve the members of a group:

'////////////////////////////////////////////////////////////////////////////////////////////////////
'    PrintAttributeWithRanging()    - Uses ADO to read an attribute with ADSI Ranging
'
'    Parameters
'
'        oDirObject  pDirObject As IDirectoryObject - Object from which the attribute will be read
'        ByVal sAttribute As String                 - Name of the attribute
'        ByVal iNumToDisplayAtATime As Integer      - Number of Attributes to retrieve at a time
'
'
Sub PrintAttributeWithRanging(oDirObject As IDirectoryObject, ByVal sAttribute As String, ByVal iNumToDisplayAtATime As Integer)
    Dim iIndex As Integer
    iIndex = 0
    Dim j, i

    Dim con As New Connection, rs As New Recordset
    Dim Com As New Command
    Dim oIADs As IADs
    Dim sAdsPathRoot As String

    ' Get the LDAP path to the passed in object
    sAdsPathRoot = GetAdsPath(oDirObject)

    'Open a Connection object
    con.Provider = "ADsDSOObject"

    '---------------------------------------------------------------------
    ' If you want to be authenticated other than current logged on user
    ' use connection properties of User ID and Password
    '---------------------------------------------------------------------
    ' con.Properties("User ID") = "Administrator"
    ' con.Properties("Password") = ""

    ' Open the connection
    con.Open "Active Directory Provider"

    ' Create a command object on this connection
    Set Com.ActiveConnection = con

    Dim iNumLastReturned As Integer
    Dim iTotalAttribs As Integer
    Dim iRange As Integer

    iTotalAttribs = 0
    iRange = 0

    Do
        ' set the query string using SQL Dialect
        Com.CommandText = "SELECT  '" & sAttribute & ";Range=" & Trim(Str(iRange)) & "-" & Trim(Str(iRange + iNumToDisplayAtATime)) & "' FROM '" & sAdsPathRoot & "' WHERE CN='*'"
        ' Tell the user what the search filter is
        DisplayMessage "Search Filter = " & Com.CommandText

        '---------------------------------------------------
        ' Or you can use LDAP Dialect, for example,
        '---------------------------------------------------
        ' Ex Com.CommandText = "<LDAP://arcadiabay1/dc=arcadiabay,DC=com>;(objectClass=*);name"
        ' For LDAP Dialect, the valid search scope are base, oneLevel and subtree
        ' Com.CommandText = "<" & adDomainPath & ">;(objectClass=*);name;subtree"
        ' For LDAP Dialect (<LDAP:...>), there is no way to specify sort order in the string,
        ' However, you can use this SORT ON property to specify sort order.
        ' for SQL Dialect you can use ORDER BY in the SQL Statement
        ' Ex. Com.Properties("Sort On") = "Name"

        'Set the preferences for Search
        Com.Properties("Page Size") = 1000
        Com.Properties("Timeout") = 30 'seconds
        Com.Properties("searchscope") = ADS_SCOPE_BASE
        Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
        Com.Properties("Cache Results") = False ' do not cache the result, it results in less memory requirements

        'Execute the query
        Set rs = Com.Execute

        ' Tell the user how many rows
        DisplayMessage "Returned " & Str(rs.RecordCount) & " rows"
        iNumLastReturned = 0
        ' Navigate the record set
        If Not rs.EOF Then
            rs.MoveFirst
        End If
        Dim f As Field
        Dim iCount As Integer
        iCount = 0

        On Error Resume Next
        While Not rs.EOF
            For Each f In rs.Fields
                ' Is the value a Variant Array?
                If TypeName(f.Value) = "Variant()" Then
                    Dim v As Variant
                    For Each v In f.Value
                        DisplayMessage f.Name & " (" & iTotalAttribs & ")" & "-" & v
                        iCount = iCount + 1
                        iNumLastReturned = iNumLastReturned + 1
                        iTotalAttribs = iTotalAttribs + 1
                    Next v
                Else
                    ' Otherwise display a single valued attribute
                    DisplayMessage f.Name & f.Value 'rs.Fields("AdsPath")
                End If
            Next f
            rs.MoveNext
        Wend
        Set rs = Nothing
        Set f = Nothing
        iRange = iRange + iNumToDisplayAtATime
    Loop While iNumLastReturned > 0

End Sub


Example Code for Ranging with IDirectorySearch

The following C++ code fragment contains a function that uses ranging to retrieve the members of a group using IDirectorySearch:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  PrintAttributeWithRanging()    - Uses IDirectorySearch to read an attribute with ADSI ranging

    Parameters

        IDirectoryObject * pDirObject   - Object from which the attribute will be read
        LPOLESTR pwszAttribute          - Name of the attribute
        int iScope                      - Scope of the search
        int iNumToDisplayAtATime        - Number of Attributes to retrieve at a time

*/
HRESULT PrintAttributeWithRanging(IDirectoryObject * pDirObject,LPOLESTR pwszAttribute, int iScope,int iNumToDisplayAtATime)
{
    HRESULT hr;
    ADS_SEARCH_COLUMN col;

    // Interface Pointers
    IDirectorySearch *pDSSearch=NULL;

    // Get an IDirectorySearch * from the passed IDirectoryObject
    hr = pDirObject->QueryInterface(IID_IDirectorySearch,(void**)&pDSSearch);
    if (FAILED(hr))
        return hr;

    // The attribute which will come back from the search
    WCHAR pszAttr[512];
    DWORD dwAttrNameSize = 1;//sizeof(pszAttr)/sizeof(LPWSTR);

    // Preferences on Search
    ADS_SEARCHPREF_INFO arSearchPrefs[1];
    DWORD dwNumPrefs;

    // Set the scope to the passed value
    arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE ;
    arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER;
    arSearchPrefs [0].vValue.Integer = ADS_SCOPE_BASE;

    // Number of elements in arSearchPrefs
    dwNumPrefs = 1;

    ADS_SEARCH_HANDLE hSearch;  // Handle for searching. ADSI uses this
                              // to keep its place

    // Set the preferences for search
    hr = pDSSearch->SetSearchPreference( arSearchPrefs, dwNumPrefs);
    if (FAILED(hr))
        return hr;

    // Do a series of Range searches, grabbing 10 at a time.
    int iRange = 0;
    DWORD dAttribsGotten = 0;
    while (SUCCEEDED(hr))
    {

        LPWSTR  pSrchFilter = (LPWSTR)&pszAttr;
        int     iAttribsGottenLastSrch =0;

        // Build the Range String : ex: L"Member;Range=0-5"
        swprintf(pSrchFilter ,L"%s;Range=%d-%d",pwszAttribute,iRange,iRange+iNumToDisplayAtATime-1);

        // Display info to the user
        wprintf(L"\nPerforming a Range IDirectorySearch for the attribute '%s'\n"
                L"The search filter is %s\n\nEntries %d through %d:\n\n"
                ,pwszAttribute,pSrchFilter,iRange,iRange+iNumToDisplayAtATime-1);

        // Executing a BASE level search
        hr = pDSSearch->ExecuteSearch(L"(CN=*)",&pSrchFilter,dwAttrNameSize, &hSearch );                                      // Execute the Search

        if ( SUCCEEDED(hr) )
        {

           while( pDSSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
           {
                LPWSTR pszColumn;
                while(pDSSearch->GetNextColumnName( hSearch, &pszColumn )!= S_ADS_NOMORE_COLUMNS )
                {        //printf("%S ", pszColumn );

                    // Get the ADsPath and save it in the list
                    hr = pDSSearch->GetColumn( hSearch, pszColumn, &col );
                    if ( SUCCEEDED(hr) )
                    {
                        for (DWORD x = 0; x< col.dwNumValues; x++)
                        {
                            wprintf(L"%d: %s\n",dAttribsGotten,(col.pADsValues+x)->CaseIgnoreString);
                            dAttribsGotten++;
                            iAttribsGottenLastSrch++;
                        }

                        // Copy the BSTR
                        // Free the column that GetColumn returns
                        pDSSearch->FreeColumn( &col );
                    }

                    FreeADsMem( pszColumn );
                }
             }

            // Close the search handle and release the IDirectorySearch interface
            pDSSearch->CloseSearchHandle(hSearch);
        }

        if (FAILED(hr) || hr ==S_ADS_NOMORE_ROWS || iAttribsGottenLastSrch== 0 || iAttribsGottenLastSrch< iNumToDisplayAtATime  )
            break;
        iRange += iNumToDisplayAtATime;
    }
    pDSSearch->Release();
    pDSSearch= NULL;
    return hr;

}


Querying for Groups in a Domain

Groups can be placed in any container or organizational unit in a domain as well as the root of the domain. This means that groups can be in numerous locations in the directory hierarchy.

You can perform a deep search for (objectClass=group) to find all groups in a tree. You can also use a query string of the form:

(&(objectClass=group)(groupType:1.2.840.113556.1.4.804: = ADS_GROUP_TYPE_SECURITY_ENABLED) )


Example Code for Performing a Query in a Domain

[C++]

The following code fragment contains two functions that perform a query against a specified root object and displays all groups and groups by type:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  QueryAndOutputGroups()   -         Gets an IDirectorySearch Interface on the passed root
                                       Performs several sample searches using IDirectorySearch

    Parameters

        IDirectoryObject * pDirObjectRoot - The root from where searches will be performed
*/
void QueryAndOutputGroups(IDirectoryObject * pDirObjectRoot)
{
    HRESULT hr;
    DisplayConsoleMsgWaitForEnter(L"To look at the new Active Directory structure, use\n"
                                  L"IDirectorySearch, to perform several querys on the directory\n"
                                  L" NOTE: All of the samples object that this program\n creates"
                                  L" begin with the text 'UGExercise'.\n\n"
                                  L"Hit ENTER to perform a subtree search of ALL the groups.\n" );

    /////////////////////////////////////////////////
    // Search for  groups

    // Interface Pointers
    IDirectorySearch *pDSSearch=NULL;

    // The attributes that are needed
    LPWSTR pszAttr[] ={  L"Name", L"ADsPath" };
    DWORD dwAttrNameSize = sizeof(pszAttr)/sizeof(LPWSTR);

    // Preferences on Search
    // Do a DEEP SUBTREE search for all accounts
    ADS_SEARCHPREF_INFO arSearchPrefs[1];
    DWORD dwNumPrefs;

    arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE ;
    arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER;
    arSearchPrefs [0].vValue.Integer = ADS_SCOPE_SUBTREE;

    dwNumPrefs = 1;

    hr = pDirObjectRoot->QueryInterface(IID_IDirectorySearch,(void**)&pDSSearch);

    if (SUCCEEDED(hr))
    {

        ////////////////////////////////////////////////
        // Search for all the groups
        puts("<<<<<<<<<<<<<<<<<<<<<ALL GROUPS>>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                   L"(objectCategory=group)",
                                   pszAttr,dwAttrNameSize,
                                   arSearchPrefs,
                                   dwNumPrefs);

        ////////////////////////////////////////////////
        // Search for all the GLOBAL groups

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of all the GLOBAL groups\n");
        puts("<<<<<<<<<<<<<<<<<<<<<ALL GLOBAL Groups>>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                    BuildGroupTypeQueryString(lmrOR,ADS_GROUP_TYPE_GLOBAL_GROUP),
                                    pszAttr,dwAttrNameSize,arSearchPrefs,
                                    dwNumPrefs);

        ////////////////////////////////////////////////
        // Search for all the DOMAIN LOCAL  groups

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of all the DOMAIN LOCAL groups\n");
        puts("<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups>>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                   BuildGroupTypeQueryString(lmrOR,ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP),
                                   pszAttr,dwAttrNameSize,
                                   arSearchPrefs,
                                   dwNumPrefs);

        if (bUseUniversalGroups)
        {
            ////////////////////////////////////////////////
            // Search for all the UNIVERSAL  groups
            DisplayConsoleMsgWaitForEnter(
                L"Hit ENTER to perform a subtree search of all the UNIVERSAL groups\n");
            puts("<<<<<<<<<<<<<<<<<<<<<ALL Universal Groups>>>>>>>>>>>>>>>>>");
            hr = SimpleDirectorySearch(pDSSearch,
                                       BuildGroupTypeQueryString(lmrOR,
                                       ADS_GROUP_TYPE_UNIVERSAL_GROUP),
                                       pszAttr,dwAttrNameSize,
                                       arSearchPrefs,
                                       dwNumPrefs);
        }

        ////////////////////////////////////////////////
        // Search for all the Security Enabled  groups

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of all Security Enabled groups\n");
        puts("<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Groups>>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                   BuildGroupTypeQueryString(lmrOR,ADS_GROUP_TYPE_SECURITY_ENABLED),
                                   pszAttr,dwAttrNameSize,
                                   arSearchPrefs,
                                   dwNumPrefs);

        ////////////////////////////////////////////////
        // Search for all the Security Enabled LOCAL groups

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of all Security Enabled LOCAL groups\n");
        puts("<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Domain Local Groups>>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                   BuildGroupTypeQueryString(lmrAND,ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP | ADS_GROUP_TYPE_SECURITY_ENABLED),
                                   pszAttr,dwAttrNameSize,
                                   arSearchPrefs,
                                   dwNumPrefs);

        ////////////////////////////////////////////////
        // Search for all the DOMAIN LOCAL groups whose
        // name begins with 'UGExercise'

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of all DOMAIN LOCAL  groups that begin with the text \'UGExercise\'\n");
        puts("<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups Beginnig with \'UGExercise\' >>>>>>>>>>>>>>>>>");

        // Build a string for the Query - this helper
        // aids in taking the BITMASK passed to it and
        // converting it into a string
        WCHAR * szwGTQuery = BuildGroupTypeQueryString(lmrOR,ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP );
        WCHAR   szwQuery[512];

        // Add the bit value to the LDAP query string
        // Look for all objects which have
        // DOMAIN LOCAL and a groupType == the domain local bitmask
        // built previously with BuildGroupTypeQueryString()
        wsprintf(szwQuery,L"(&(cn=UGExercise*)%s)",szwGTQuery);
        OutputDebugString(szwQuery);
        OutputDebugString(L"\r\n");

        // Perform the search using the query string built previously (szwQuery)
        hr = SimpleDirectorySearch(pDSSearch,szwQuery,pszAttr,dwAttrNameSize,arSearchPrefs,dwNumPrefs);

        ////////////////////////////////////////////////
        // Search for ALL the groups whose
        // name begins with 'UGExercise'

        DisplayConsoleMsgWaitForEnter(
            L"Hit ENTER to perform a subtree search of ALL groups that begin with the text \'UGExercise \'\n");

        puts("<<<<<<<<<<<<<<<<<<<<<ALL Groups Beginning with \'UGExercise \' >>>>>>>>>>>>>>>>>");
        hr = SimpleDirectorySearch(pDSSearch,
                                   L"(&(cn=UGExercise*)(objectCategory=group))",
                                   pszAttr,dwAttrNameSize,
                                   arSearchPrefs,
                                   dwNumPrefs);

        // Release the Search interface
        pDSSearch->Release();
        pDSSearch= NULL;
    }
    else
        puts("ERROR: pDirObjectRoot->QueryInterface(IID_IDirectorySearch,(void**)&pDSSearch); FAILED!");

}
////////////////////////////////////////////////////////////////////////////////////////////////////
/*  SimpleDirectorySearch()   - Uses IDirectorySearch to search the directory
                                results are printed to the console window
    Parameters

        IDirectorySearch *pDSSearch         - IDirectorySearch interface from the object
                                              which will serve as the root of the search
        LPWSTR pszSearchFilter              - LDAP Search filter
        LPWSTR pszAttr[]                    - Array of Attribute names to retrieve
        DWORD dwAttrNameSize                - Number of elements in pszAttr
        PADS_SEARCHPREF_INFO pSearchPrefs   - ADSI Array of preferences
        DWORD dwNumPrefs                    - Number of elements in pSearchPrefs
*/
HRESULT SimpleDirectorySearch(IDirectorySearch *pDSSearch,LPWSTR pszSearchFilter, LPWSTR pszAttr[],DWORD dwAttrNameSize,
                                PADS_SEARCHPREF_INFO pSearchPrefs,DWORD dwNumPrefs  )
{
    wprintf(L"\nSearch Filter =%s\n",pszSearchFilter);
    // COL for iterations
    ADS_SEARCH_COLUMN col;
    HRESULT hr;

    // Interface Pointers
    IADs    *pObj = NULL;
    IADs    * pIADs = NULL;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;

    // if the preferences were passed in, call IDirectorySearch::SetSearchPreference()
    if (dwNumPrefs &&  pSearchPrefs)
    {
        hr = pDSSearch->SetSearchPreference( pSearchPrefs, dwNumPrefs);
        if (FAILED(hr))
            return hr;
    }
    // Execute the search using the passed search filter, the
    // attributes and the address of the search handle
    hr = pDSSearch->ExecuteSearch(pszSearchFilter,pszAttr ,dwAttrNameSize, &hSearch );

    if ( SUCCEEDED(hr) )
    {

    // Call IDirectorySearch::GetNextRow() to retrieve the next row
    //of data
        while( pDSSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
        {

            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++)
            {

                // Get the data for this column
                hr = pDSSearch->GetColumn( hSearch, pszAttr[x], &col );

                if ( SUCCEEDED(hr) )
                {

                    // Print the data for the column and free the column
                    wprintf(L"%s: %s\r\n",pszAttr[x],col.pADsValues->CaseIgnoreString);
                    pDSSearch->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property NOT available>",pszAttr[x]);
            }
            puts("------------------------------------------------");
        }

        // Close the search handle to clean up
        pDSSearch->CloseSearchHandle(hSearch);
    }
    return hr;
}
[Visual Basic]

The following subroutine performs a query against a specified root object and display all groups and groups by type:

'************************************
'  QueryAndOutputGroups()   -           Performs several sample searches using ADO
'
'
'    Parameters
'
'        oDirObjectRoot As IDirectoryObject - The root from where searches will be performed
'
Sub QueryAndOutputGroups(oDirObjectRoot As IDirectoryObject)

    '*******************************
    ' Search for all the groups
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL GROUPS>>>>>>>>>>>>>>>>>" 'cn = 'UG*' objectCategory
    SimpleDirectorySearch oDirObjectRoot, "objectCategory = 'group'", ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the GLOBAL groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the GLOBAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL GLOBAL Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_GLOBAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the DOMAIN LOCAL  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the DOMAIN LOCAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '        #ifdef USE_UNIVERSAL_GROUPS

    '*****************************************
    ' Search for all the UNIVERSAL  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all the UNIVERSAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Universal Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_UNIVERSAL_GROUP) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_UNIVERSAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE
    '        #End If

    '*****************************************
    ' Search for all the Security Enabled  groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all Security Enabled groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_SECURITY_ENABLED) & " or " & _
                            "GroupType=" & Str(ADS_GROUP_TYPE_SECURITY_ENABLED Or ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the Security Enabled LOCAL groups
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all Security Enabled LOCAL groups"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Security Enabled Domain Local Groups>>>>>>>>>>>>>>>>>"
    SimpleDirectorySearch oDirObjectRoot, "GroupType=" & Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or _
                            ADS_GROUP_TYPE_SECURITY_ENABLED), _
                            ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for all the DOMAIN LOCAL groups whose
    ' name begins with 'UGExercise'
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of all DOMAIN LOCAL  groups that begin with the text 'UGExercise'"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Domain Local Groups Beginnig with 'UGExercise' >>>>>>>>>>>>>>>>>"

    SimpleDirectorySearch oDirObjectRoot, "cn='UGExercise*' and (GroupType=" & _
                            Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) & " or GroupType=" & _
                            Str(ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED) & _
                            ")", ADS_SCOPE_SUBTREE

    '*****************************************
    ' Search for ALL the groups whose
    ' name begins with 'UGExercise'
    DisplayMessage " "
    DisplayMsgWaitForInput "Hit OK to perform a subtree search of ALL groups that begin with the text 'UGExercise '"
    DisplayMessage " "
    DisplayMessage "<<<<<<<<<<<<<<<<<<<<<ALL Groups Beginning with 'UGExercise' >>>>>>>>>>>>>>>>>"

    SimpleDirectorySearch oDirObjectRoot, " cn='UGExercise*' and objectCategory='group'", ADS_SCOPE_SUBTREE

End Sub

'////////////////////////////////////////////////////////////////////////////////////////////////////
'   SimpleDirectorySearch()   - Uses ADO to search the directory
'                               Results are displayed in the list box
'    Parameters
'
'        oDirObjectRoot As IDirectoryObject  - Root of the search
'        ByVal sSearchFilter As String       - LDAP Search filter
'        ByVal lScope As Integer             - Scope for Searching possible values are:
'                                                ADS_SCOPE_BASE
'                                                ADS_SCOPE_ONELEVEL
'                                                ADS_SCOPE_SUBTREE
'
'        (See IDirectorySearch and ADS_SCOPEENUM in the docs for more info)
'
Sub SimpleDirectorySearch(oDirObjectRoot As IDirectoryObject, ByVal sSearchFilter As String, ByVal lScope As Integer)

    Dim iIndex As Integer
    iIndex = 0
    Dim v, j, i

    Dim con As New Connection, rs As New Recordset
    Dim Com As New Command
    Dim oIADs As IADs
    Dim sAdsPathRoot As String

    ' Get the LDAP path to the passed in object
    sAdsPathRoot = GetAdsPath(oDirObjectRoot)

    'Open a Connection object
    con.Provider = "ADsDSOObject"

    '---------------------------------------------------------------------
    ' If you want to be authenticated other than current logged on user
    ' use connection properties of User ID and Password
    '---------------------------------------------------------------------
    ' con.Properties("User ID") = "Administrator"
    ' con.Properties("Password") = ""

    ' Open the connection
    con.Open "Active Directory Provider"

    ' Create a command object on this connection
    Set Com.ActiveConnection = con

    ' set the query string using SQL Dialect
    Com.CommandText = "select name,AdsPath from '" & sAdsPathRoot & "' where " & sSearchFilter & " ORDER BY NAME"

    ' Tell the user what the search filter is
    DisplayMessage "Search Filter = " & Com.CommandText

    '---------------------------------------------------
    ' Or you can use LDAP Dialect, for example,
    '---------------------------------------------------
    ' Ex Com.CommandText = "<LDAP://arcadiabay1/dc=arcadiabay,DC=com>;(objectClass=*);name"
    ' For LDAP Dialect, the valid search scope are base, oneLevel and subtree
    ' Com.CommandText = "<" & adDomainPath & ">;(objectClass=*);name;subtree"
    ' For LDAP Dialect (<LDAP:...>), there is no way to specify sort order in the string,
    ' However, you can use this SORT ON property to specify sort order.
    ' for SQL Dialect you can use ORDER BY in the SQL Statement
    ' Ex. Com.Properties("Sort On") = "Name"

    'Set the preferences for Search
    Com.Properties("Page Size") = 1000
    Com.Properties("Timeout") = 30 'seconds
    Com.Properties("searchscope") = lScope
    Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
    Com.Properties("Cache Results") = False ' do not cache the result, it results in less memory requirements

    'Execute the query
    Set rs = Com.Execute

    ' Tell the user how many rows
    DisplayMessage "Returned " & Str(rs.RecordCount) & " rows"

    ' Navigate the record set
    If Not rs.EOF Then
        rs.MoveFirst
    End If

    On Error Resume Next
    While Not rs.EOF
        ' Display the LDAP path for the row
        DisplayMessage rs.Fields("AdsPath")
        rs.MoveNext
    Wend

End Sub


Changing a Group's Scope or Type

Changing a group's scope or type is not allowed in mixed-mode domains. However, the following conversions are allowed in native-mode domains:

Global group to universal group. However, this is only allowed if the global group is not a member of another global group.

Domain local group to universal group. However, the domain local group being converted cannot contain another domain local group.

Universal group to global or domain local group. For conversion to global group, the universal group being converted cannot contain users or global groups from another domain. For conversion to domain local group, the universal group being converted cannot be a member of any universal group or a domain local group from another domain.

In native-mode, a group's type can be converted freely between security groups and distribution groups.

Note that if a group is used to set access control, changing the scope or type can affect the access control entries (ACEs) that contain that group. The security system will ignore ACEs that contain groups that are not security groups.


Example Code for Changing the Scope of a Group

[C++]

The following code fragment changes the scope of a group:

WCHAR pwszLDAPPath[MAX_PATH*2];

    HRESULT hr;
    IADsGroup * pGroup = NULL;

    // Initialize COM
    CoInitialize(0);

    // Bind to the container passed
hr = ADsGetObject( pwszLDAPPath, IID_IADsGroup,(void **)&pGroup);

    if (SUCCEEDED(hr))
    {
        VARIANT vValue;
        BSTR    bsValue = SysAllocString(L"groupType");
        VariantInit(&vValue);
    // Set a new GroupType Value
        vValue.vt = VT_I4;
        vValue.lVal = ADS_GROUP_TYPE_GLOBAL_GROUP ;

        hr = pGroup->Put(bsValue,vValue);
        hr = pGroup->SetInfo();
        pGroup->Release();
        pGroup= NULL;
    }

    CoUninitialize();
[Visual Basic]

The following code fragment changes the scope of a group:

Dim x as IADs
Set x = GetObject("LDAP://CN=mygroup,OU=myou,DC=arcadiabay,DC=com")
x.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP|ADS_GROUP_TYPE_SECURITY_ENABLED


Deleting Groups

To delete a group from a domain, bind to the container containing the group and specify "group" as the class using IADsContainer::Delete to delete the group. Note that you do not need to call IADs::SetInfo method to commit the change to the container. The IADsContainer::Delete call commits the deletion of the group directly to the directory.


Example Code for Deleting a Group in a Domain

[C++]

The following code fragment contains a function that deletes a group in a domain:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  DeleteADObject()   - Deletes the passed object by AdsPath

    Parameters

        LPOLESTR pwszAdsPath        -       AdsPath of object to delete

    Optional Parameters:

       LPOLESTR pwszUser            - User Name and Password, if the parameters are NOT passed,
       LPOLESTER pwszPassWord       - Binding will use ADsGetObject, if the parameters
                                    - Are specified, will use ADsOpenObject, passing user name and password

*/
HRESULT DeleteADObject(LPOLESTR pwszAdsPath, LPOLESTR  pwszUser,LPOLESTR  pwszPassWord)
{
    HRESULT             hr;
    BSTR                bsParentPath;
    IADs *              pIADsToDelete = NULL;
    IDirectoryObject *  pIDirObjectParent= NULL;
    VARIANT             vCNToDelete;
    WCHAR               pwszTemp[512];

    VariantInit(&vCNToDelete);
    OutputDebugString(pwszAdsPath);
    OutputDebugString(L"\r\n");

    // Bind to the object being deleted

    assert((pwszUser==NULL && pwszPassWord == NULL) || (pwszUser && pwszPassWord));

    // If a username and password are passed in, use ADsOpenObject()
    // otherwise use ADsGetObject()
    if (!pwszUser) // No user password passed, use ADsOpenObject
    {
        hr = ADsGetObject(  pwszAdsPath, IID_IADs,(void **)& pIADsToDelete);
    }
    else
    {
        hr = ADsOpenObject(pwszAdsPath, pwszUser, pwszPassWord,
                           ADS_SECURE_AUTHENTICATION,IID_IADs, (void**) & pIADsToDelete);
    }

    if (SUCCEEDED(hr))
    {
       // Get the parent path
        hr = pIADsToDelete->get_Parent(&bsParentPath);

        // Get the CN property for the object to delete
        hr = pIADsToDelete->Get(L"cn",&vCNToDelete);
        if (SUCCEEDED(hr))
        {
            // ************************************************************
            // Now bind to the parent
            // If a username and password are passed in, use ADsOpenObject()
            // otherwise use ADsGetObject()
            if (!pwszUser) // No user password passed, use ADsOpenObject
            {
                hr = ADsGetObject(  bsParentPath, IID_IDirectoryObject,(void **)& pIDirObjectParent);
            }
            else
            {
                hr = ADsOpenObject(bsParentPath, pwszUser, pwszPassWord,
                                   ADS_SECURE_AUTHENTICATION,IID_IDirectoryObject, (void**) & pIDirObjectParent);
            }
            if (SUCCEEDED(hr))
            {
                // Release the object to delete
                pIADsToDelete->Release();
                pIADsToDelete =NULL;

                // Put the CN property into a string beginning with CN=
                swprintf(pwszTemp,L"cn=%s\n",vCNToDelete.bstrVal);

                // Ask the parent to delete the child
                hr =pIDirObjectParent->DeleteDSObject(pwszTemp);
                // Release the Parent Object
                pIDirObjectParent->Release();
                pIDirObjectParent = NULL;
            }
        }
        SysFreeString(bsParentPath);
    }
    // If we have a IADsObject- we need to release it
    if ( pIADsToDelete)
    {
        // Release the object to delete
        pIADsToDelete->Release();
        pIADsToDelete =NULL;
    }

    VariantClear(&vCNToDelete);
 return hr;
}
[Visual Basic]

The following code deletes a group in domain:

Dim x as IADs
Set x = GetObject("LDAP://OU=myou,DC=arcadiabay,DC=com")
x.Delete("group", "cn=mygroup")


Moving Groups

Groups can be moved within a domain.

However, you can only move objects between domains using the movetree.exe utility and then using that utility only universal groups can be moved from one domain to another. Note that a global or domain local group can be converted to a universal group, moved to another domain, and converted back again.

To move a group

  1. Bind to the group to move and get an IADs pointer.
  2. Get the ADsPath using the IADs::get_ADsPath property method. The ADsPath will be used to specify the group to move.
  3. Bind to the container object where you want to move the group to and get an IADsContainer pointer.
  4. Move the group using the IADsContainer::MoveHere method.

    Note that if you have a pointer to the group object before it was moved, the pointer to the object is still valid but the object's methods will no longer be valid since the directory object it represents is no longer valid.


Example Code for Moving a Group

[C++]

The following code fragment contains a function that moves a group (or any object) to another location in the domain:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*
    MoveObject()      -   Moves an object in the directory from the IDirectoryObject passed
                          The Object at the NEW location is Queried for an updated
                          IDirectoryObject interface, and the passed PTR is updated..
    Parameters

        IDirectoryObject *pIDODestination    - Place to move the object to
        IDirectoryObject **ppIDOToMove       - Object to move (note returned is a different PTR
                                                (but it points to the moved object)
*/

HRESULT MoveObject(IDirectoryObject * pIDODestination,IDirectoryObject **ppIDOToMove)
{
    IADsContainer * pIADsContainerDestination = NULL;
    IADs          * pIADsToMove;
    HRESULT         hr;
    BSTR            bsPathToMove;

    // QI a IADs ptr from the ToMove object
    hr =(*ppIDOToMove)->QueryInterface(IID_IADs,(void**)&pIADsToMove);

    if (SUCCEEDED(hr))
    {

        //The ADS path of the object to MOVE
        hr =pIADsToMove->get_ADsPath(&bsPathToMove);
        if (SUCCEEDED(hr))
        {

           //printf("\n%s\n",QueryAllInterfaces( pIDODestination));
            // Get an IADsContainer * from the Destination
            hr =pIDODestination->QueryInterface(IID_IADsContainer ,(void**)&pIADsContainerDestination );
            if (SUCCEEDED(hr))
            {
                IDispatch * pIDispatchNewObject = NULL;

                // Actually MOVE the object
                hr = pIADsContainerDestination->MoveHere(bsPathToMove,NULL,&pIDispatchNewObject );
                if (SUCCEEDED(hr))
                {
                    IDirectoryObject * pIDONewOneToReturn = NULL;

                    // Now get a IDirectoryObject * from
                    // the IDispatch returned from MoveHere()
                    hr =pIDispatchNewObject->QueryInterface(IID_IDirectoryObject  ,(void**)pIDONewOneToReturn );

                    if (SUCCEEDED(hr))
                    {

                        // Now take the PASSED interface. FREE it and reassign the new ptr
                        // (note if there are ANY outstanding refs to the com object behind the ptr- they will still be
                        // valid (from a COM point of view), but this object will return errors if you try to do anything
                        // with it)
                        (*ppIDOToMove)->Release();

                        // Copy the ptr from the QI'd one to the return for this function
                        ppIDOToMove = &pIDONewOneToReturn;
                    }
                    pIDispatchNewObject->Release();
                    pIDispatchNewObject = NULL;
                }

                pIADsContainerDestination->Release();
                pIADsContainerDestination  = NULL;
            }
            SysFreeString(bsPathToMove);
        }
        pIADsToMove->Release();
        pIADsToMove = NULL;
    }

    return hr;
}
[Visual Basic]

The following code fragment moves a group (or any object) to another location in the domain:

    Dim oDirObjTo As IADsContainer
    Dim oNewObj As IADs

    Set oDirObjTo = GetObject(txtTo.Text)

    Set oNewObj = oDirObjTo.MoveHere(txtFrom.Text, vbNullString)

    MsgBox "New Object has been moved to " & oNewObj.ADsPath

    Set oDirObjTo = Nothing

    Set oNewObj = Nothing


Getting the Domain Account-Style Name of a Group

Users, groups, computers, and other security principals can also be represented in domain account form. Domain account (the logon name used in previous versions of Windows NT®) has the following form:

domain\account

where domain is the name of the Windows NT domain that contains the user and account is the samAccountName property of the specified user. For example: arcadiabay\jsmith.

The domain account form can be used to specify the trustee in an ACE in a security descriptor. It is also used for the logon name on computers running Windows version NT 4.0 and earlier.

//Need to include the following headers to use DsGetDcName
//#include <LMCONS.H>
//#include <dsgetdc.h>
//#include <lmapibuf.h>
//This function returns the previous version name of the security principal
//specified by the distinguished name specified by szDN.
//The szDomain parameter should be NULL to use the current domain
//to get the name translation. Otherwise, you should specify the
//domain you want to use as just the domain name (such as northwestdom)
//or in dotted format (such as northwestdom.arcadiabay.com).
HRESULT GetDownlevelName(LPOLESTR szDomainName, LPOLESTR szDN, LPOLESTR *ppNameString)
{
HRESULT hr = E_FAIL;
IADsNameTranslate *pNameTr = NULL;
IADs *pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
LPOLESTR szInitDomain = new OLECHAR[MAX_PATH];
BSTR szNameTr;

if ((!szDN)||(!ppNameString))
    return hr;

//Use the current domain if none is specified.
if (!szDomainName)
{
    //Call DsGetDcName to get the name of this computer's domain.
    PDOMAIN_CONTROLLER_INFO DomainControllerInfo = NULL;
    DWORD dReturn = 0L;
    dReturn = DsGetDcName(  NULL,
                NULL,
                NULL,
                NULL,
                DS_DIRECTORY_SERVICE_REQUIRED,
                &DomainControllerInfo
    );
    if (dReturn==NO_ERROR)
    {
        wcscpy(szInitDomain, DomainControllerInfo->DomainName);
        hr = S_OK;
    }

    //Free the buffer.
    if (DomainControllerInfo)
        NetApiBufferFree(DomainControllerInfo);
}
else
{
    wcscpy(szInitDomain, szDomainName);
    hr = S_OK;
}

if (SUCCEEDED(hr))
{

    //Create the COM object for the IADsNameTranslate object.
    hr  = CoCreateInstance(
                                CLSID_NameTranslate,
                                NULL,
                                CLSCTX_INPROC_SERVER,
                                IID_IADsNameTranslate,
                                (void **)&pNameTr
                          );
    if (SUCCEEDED(hr))
    {

        //Initialize for the specified domain.
        hr = pNameTr->Init(ADS_NAME_INITTYPE_DOMAIN, szInitDomain);
        if (SUCCEEDED(hr))
        {
            hr = pNameTr->Set(ADS_NAME_TYPE_1779, szDN);
            hr = pNameTr->Get(ADS_NAME_TYPE_NT4, &szNameTr);
            if (SUCCEEDED(hr))
            {
                *ppNameString = (OLECHAR *)CoTaskMemAlloc (sizeof(OLECHAR)*(wcslen(szNameTr)+1));
                if (*ppNameString)
                    wcscpy(*ppNameString, szNameTr);
                else
                    hr=E_FAIL;
            }
        }
    }
    if (pNameTr)
       pNameTr->Release();
}

//Caller must call CoTaskMemFree to free ppNameString.
return hr;
}


Groups on Member Servers and Windows 2000 Professional

On member servers and Windows® 2000 Professional, there is a local security database. That local security database can contain its own local user and machine local groups whose scope is only the particular computer where they are created. When managing these types of users and groups on member servers and computers running Windows NT® Workstation/Windows 2000 Professional, you use the WinNT provider.

When a member server or a computer running Windows 2000 Professional or Windows 2000 Professional is a member of a Windows 2000 domain, the groups or users in the domain can be used in the local security database to grant rights to that group on that particular computer.

When managing groups on a Windows 2000 domain using ADSI, you use the LDAP provider. When managing groups on member servers and computers running Windows NT Workstation/Windows 2000 Professional, you use the WinNT provider.

This means you need to bind at least once to each provider: 1) Bind to the LDAP provider to retrieve the ADsPath to the group or user you want to add to a group in the local database and 2) Bind to the WinNT provider to add that user or group to a machine local group.


Enumerating Groups on Member Servers and Windows 2000 Professional

On member servers and computers running Windows 2000 Professional, you can easily enumerate all the machine local groups.

Note that only machine local groups can be created on member servers and Windows 2000 Professional. However, those machine local groups can contain 1) Universal and Global groups from the forest containing the domain that the computer is a member of or 2) Domain local groups from that computer's domain 3) Users from any domain in the forest.

To enumerate the machine local groups on a member server or computer running Windows 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName, computer

      where sComputerName is the name of the computer who groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer. ADSI makes this information available to the WinNT provider's parser so that it can skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Set a filter containing "groups" using the IADsContainer::put_Filter property method. This enables you to enumerate the container and retrieve only groups.
  3. Enumerate the group objects, using the IADsContainer::get__NewEnum method.
  4. For each the group object, using the IADsGroup methods to read the name and members of the group.


Example Code for Enumerating Groups

[C++]

The following code fragment contains a function that enumerates all objects of a specified class (such as group) and displays the members contained in each object on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  ListMembersWithWinNtProvider()    - Uses the WinNT provider to list children based on a filter
                                        Returns S_OK on success

    Parameters

    LPWSTR pwszComputer     - Computer to list
    LPWSTR pwszClass        - Filter for listing
    LPWSTR pwszUSER = NULL  - User Name for ADsOpenObject() binding- If NOT passed - Bind Though ADsGetObject()
    LPWSTR pwszPASS = NULL  - Password for ADsOpenObject() binding- If NOT passed - Bind Though ADsGetObject()

*/
HRESULT ListMembersWithWinNtProvider(LPWSTR pwszComputer,LPWSTR pwszClass, LPWSTR pwszUSER = NULL, LPWSTR pwszPASS = NULL)
{
    HRESULT hr;
    LPWSTR  pwszBindingString = NULL;

    IADsContainer * pIADsCont = NULL;

    // Allocate a String for Binding.. This should definitely be big enough..
    pwszBindingString = new WCHAR[(wcslen(gbsComputer) *2) + 20];

    swprintf(pwszBindingString,L"WinNT://%s,computer",pwszComputer);

    // Make sure either NO user is passed - or BOTH user and password are passed
    assert(!pwszUSER || (pwszUSER && pwszPASS));

    // Bind to the container passed
    // If USER and PASS passed in, use ADsOpenObject()
    if (pwszUSER)
        hr = ADsOpenObject( pwszBindingString,
                            pwszUSER,
                            pwszPASS,
                            ADS_SECURE_AUTHENTICATION,
                            IID_IADsContainer,
                            (void**) &pIADsCont);
    else
        hr = ADsGetObject( pwszBindingString, IID_IADsContainer,(void **)&pIADsCont);

    if (SUCCEEDED(hr))
    {
        VARIANT vFilter;
        VariantInit(&vFilter);
        LPWSTR pwszFilter = pwszClass;

        // Build a Variant of array type, using the filter passed
        hr = ADsBuildVarArrayStr(&pwszFilter, 1, &vFilter);

        if (SUCCEEDED(hr))
        {
            // Set the filter for the results of the Enum
            hr = pIADsCont->put_Filter(vFilter);

            if (SUCCEEDED(hr))
            {
                IEnumVARIANT * pEnumVariant = NULL; // Ptr to the IEnumVariant Interface
                VARIANT Variant;                    // Variant for retrieving data
                ULONG   ulElementsFetched;          // Number of elements fetched

                // Builds an enumerator interface- this will be used
                // to enumerate the objects contained in the IADsContainer
                hr = ADsBuildEnumerator(pIADsCont,&pEnumVariant);
                // While no errors- Loop through and print the data
                while (SUCCEEDED(hr) && hr != S_FALSE)
                {

                    // Object comes back as a VARIANT holding an IDispatch *
                    hr = ADsEnumerateNext(pEnumVariant,1,&Variant,&ulElementsFetched);

                    if (hr != S_FALSE)
                    {
                        assert(HAS_BIT_STYLE(Variant.vt,VT_DISPATCH));

                        IDispatch *pDispatch = NULL;
                        IADs *pIADs= NULL;
                        pDispatch = Variant.pdispVal;

                        // QI the Variant's IDispatch * for the IADs interface
                        hr = pDispatch->QueryInterface(IID_IADs,(VOID **) &pIADs) ;

                        if (SUCCEEDED(hr))
                        {
                            // Print some information about the object
                            BSTR bsResult;

                            pIADs->get_Name(&bsResult);
                            wprintf(L" NAME: %s\n",(LPOLESTR) bsResult);
                            SysFreeString(bsResult);

                            pIADs->get_ADsPath(&bsResult);
                            wprintf(L" ADSPATH: %s\n",(LPOLESTR) bsResult);
                            SysFreeString(bsResult);

                            puts("------------------------------------------------------");
                            pIADs->Release();
                            pIADs = NULL;
                        }
                    }
                }

                // Since the hr from iteration was lost, free
                // the interface if the ptr is != NULL
                if (pEnumVariant)
                {
                    pEnumVariant->Release();
                    pEnumVariant = NULL;
                }
                VariantClear(&Variant);
            }
        }
        VariantClear(&vFilter);
    }

    delete [] pwszBindingString;
    pwszBindingString = NULL;

    return hr;
}
[Visual Basic]

The following code enumerates all groups and displays the members contained in each group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

'Example: Enumerating all local groups on member server or Windows NT Workstation/Windows 2000 Professional
Dim IADsCont As IADsContainer
Dim Group As IADsGroup

sComputer = InputBox("This script lists the groups on a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name:")

If sComputer = "" Then
  MsgBox "No computer name was specified. You must specify a computer name."
  Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
'Note that this sample uses the logged-on user's context
'You should use IADsOpenDSObject to use a specific account
'that is not the logged-on user's account.
Set IADsCont = GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Filter to view only group objects
'''''''''''''''''''''''''''''''''''''''
IADsCont.Filter = Array("group")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsContainer::Filter method"
End If

strText = ""
intIndex = 0
intNumDisplay = 0
cmember = 0
'Maximum number of groups to list on a msgbox.
MAX_DISPLAY = 10

'''''''''''''''''''''''''''''''''''''''
'Get each group and display its name and its members
'''''''''''''''''''''''''''''''''''''''
For Each Group In IADsCont
    intIndex = intIndex + 1
    'Get the name
    strText = strText & vbCrLf & Right("  " & intIndex, 4) & " " & Group.Name

    intNumDisplay = intNumDisplay + 1
    'Get the members object
    Set memberList = Group.members
    If (Err.Number <> 0) Then
       BailOnFailure Err.Number, "on IADsGroup::members method"
    End If

    'Get the enumerate the members of the group from the members object
    For Each member In memberList
      If cmember = 0 Then
    strText = strText & vbCrLf & "       " & "Members:"
      End If
      strText = strText & vbCrLf & "       " & member.Name & " (" & member.Class & ")"
      cmember = cmember + 1
    Next
    If cmember = 0 Then
      strText = strText & vbCrLf & "       " & "No members"
    End If
    'Display in msgbox if there are MAX_DISPLAY groups to display
    If intNumDisplay >= MAX_DISPLAY Then
        Call show_groups(strText, sComputer)
        strText = ""
        intNumDisplay = 0
    End If
    'Reset the count of members within the current group
    cmember = 0
Next
Call show_groups(strText, sComputer)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_groups(strText, strName)
    MsgBox strText, vbInformation, "Groups on " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


Creating Machine Local Groups on Member Servers and Windows 2000 Professional

Only machine local groups (class localGroup) can be created on member servers and Windows® 2000 Professional.

To create a machine local group on a member server or computer running Windows 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName , computer

      where sComputerName is the name of the computer who groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer. ADSI makes this information available to the WinNT provider's parser so that it can skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Specify "localGroup" as the class using IADsContainer::Create to add the group.

    Note that if you specify "group" as the class, ADSI will use "localGroup". Do not specify the class as "globalGroup". Groups of class "globalGroup" cannot be created on member servers or a computer running Windows NT Workstation/Windows 2000 Professional. Note that if you specify "globalGroup," IADsContainer::Create will create the group in the property cache but IADs::SetInfo will not write the group to the security database and it will not return an error.

  3. Write the group to the computer's security database using IADs::SetInfo.


Example Code for Creating a Group on a Member Server or Windows NT Workstation/Windows 2000 Professional

[C++]

The following code fragment creates a group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

void wmain( int argc, wchar_t *argv[ ])
{
    HRESULT hr;
    IDirectoryObject * pDirObjectRoot           = NULL;
    IDirectoryObject * pDirObjectSampleLocal    = NULL;
    BOOL               bMixedMode;
    IADsContainer    * pIADsCont                = NULL;

    // Initialize COM
    CoInitialize(0);
    IUnknown * pUnk;

    // Since we are using the WinNT provider, bind to the specific computer name,
    // put your computer name in the string below
    #ifdef USE_OPEN_OBJECT
        hr = ADsOpenObject(L"WinNT://MyComputerName,computer", ADS_OPENOBJECT_USERNAME, ADS_OPENOBJECT_PASSWORD ,
                                                           ADS_SECURE_AUTHENTICATION,IID_IADsContainer, (void**) &pIADsCont);
    #else
        hr = ADsGetObject(L"WinNT:// MyComputerName,computer", IID_IADsContainer, (void**) &pIADsCont);
    #endif

    if (SUCCEEDED(hr))
    {
        IDispatch * pIDispatch = NULL;

        BSTR bsGroup = SysAllocString(L"group");
        BSTR bsName = SysAllocString(L"ThisIsAGroup");

        hr = pIADsCont->Create(bsGroup,bsName , &pIDispatch);

        if (SUCCEEDED(hr))
        {
            puts(QueryAllInterfaces(pIDispatch));

            IADsGroup   * pGroup = NULL;

            hr = pIDispatch->QueryInterface(IID_IADsGroup,(void**)&pGroup);

            if (SUCCEEDED(hr))
            {
                hr = pGroup->SetInfo();

                if(SUCCEEDED(hr))
                {
                    // NOW- add some users (fill these in with Valid Users)
                    BSTR bsUserOne = SysAllocString(L"WinNT://MyComputerName /Administrator");
                    BSTR bsUserTwo = SysAllocString(L"WinNT://MyComputerName /jcooper");
                    hr = pGroup->Add(bsUserOne);
                    if (SUCCEEDED(hr))
                    {
                        hr = pGroup->Add(bsUserTwo);
                        if (SUCCEEDED(hr))
                        {
                            IADsMembers *   pADsMembers       = NULL;     // Ptr to Members of the IADsGroup
                            BOOL            fContinue         = TRUE;     // Looping Variable
                            IEnumVARIANT *  pEnumVariant      = NULL;     // Ptr to the Enum variant
                            IUnknown *      pUnknown          = NULL;     // IUnknown for getting the ENUM initially
                            VARIANT         VariantArray[FETCH_NUM];      // Variant array for temp holding returned data
                            ULONG           ulElementsFetched = NULL;     // Number of elements fetched

                            // Get a interface pointer to the IADsCollection of members
                            hr = pGroup->Members(&pADsMembers);
                            if (SUCCEEDED(hr))
                            {
                                // Ask the IADsCollection of members for a new ENUM Interface
                                // Note the enum comes back as an IUnknown *
                                hr = pADsMembers->get__NewEnum(&pUnknown);
                                if (SUCCEEDED(hr))
                                {
                                    // QI the IUnknown * for a IEnumVARIANT interface
                                    hr = pUnknown->QueryInterface(IID_IEnumVARIANT, (void **)&pEnumVariant);
                                    if (SUCCEEDED(hr))
                                    {
                                        // While have not hit errors or end of data....
                                        while (fContinue)
                                        {
                                           ulElementsFetched = 0;
                                            // Get a "batch" number of group members- number of rows specified by FETCH_NUM
                                            hr = ADsEnumerateNext(pEnumVariant, FETCH_NUM, VariantArray, &ulElementsFetched);

                                            if (ulElementsFetched )//SUCCEEDED(hr) && hr != S_FALSE)
                                            {
                                                // Loop through the current batch- printing the path
                                                // for each member
                                                for (ULONG i = 0; i < ulElementsFetched; i++ )
                                                {
                                                    IDispatch * pDispatch         = NULL; // ptr for holding dispath of element
                                                    IADs      * pIADsGroupMember  = NULL; // IADs ptr to group member
                                                    BSTR        bstrPath          = NULL; // Holds path of object

                                                    // Get the dispatch ptr for the variant
                                                    pDispatch = VariantArray[i].pdispVal;
                                                    assert(HAS_BIT_STYLE(VariantArray[i].vt,VT_DISPATCH));

                                                    // Get the IADs interface for the "member" of this group
                                                    hr = pDispatch->QueryInterface(IID_IADs,
                                                                                (VOID **) &pIADsGroupMember) ;
                                                    if (SUCCEEDED(hr))
                                                    {
                                                        // Get the ADsPath property for this member
                                                        hr = pIADsGroupMember->get_ADsPath(&bstrPath) ;
                                                        if (SUCCEEDED(hr))
                                                        {
                                                            // Print the ADsPath of the group member
                                                            printf("\tMember Object: %ws\n", bstrPath);
                                                            SysFreeString(bstrPath);
                                                        }
                                                        pIADsGroupMember->Release();
                                                        pIADsGroupMember   = NULL;
                                                    }
                                                 }
                                                // Clear the variant array
                                                memset(VariantArray, 0, sizeof(VARIANT)*FETCH_NUM);
                                            }
                                            else
                                                fContinue = FALSE;
                                        }
                                        pEnumVariant->Release();
                                        pEnumVariant = NULL;
                                    }
                                    pUnknown->Release();
                                    pUnknown = NULL;
                                }
                                pADsMembers ->Release();
                                pADsMembers  = NULL;
                            }
                        }
                    }
                    SysFreeString(bsUserOne);
                    SysFreeString(bsUserTwo);
                    bsUserOne = NULL;
                    bsUserTwo = NULL;
                }
                pGroup->Release();
                pGroup = NULL;
            }

            pIDispatch->Release();
            pIDispatch = NULL;
        }
        pIADsCont->Release();
        pIADsCont = NULL;
    }
    CoUninitialize();
}
[Visual Basic]

The following code creates a group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

On Error Resume Next

Dim IADsCont As IADsContainer
Dim oIADsGroup As IADsGroup
Dim ListIADsMembers As IADsMembers
Dim Member As IADs

sComputer = InputBox("This script creates a group on a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name:")
sGroup = InputBox("Specify the group name:")

If sComputer = "" Then
  MsgBox "No computer name was specified. You must specify a computer name."
  Exit Sub
End If
If sGroup = "" Then
  MsgBox "No group name was specified. You must specify a group name."
  Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
Set IADsCont = GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Create the group
'''''''''''''''''''''''''''''''''''''''
'Only Local groups can be created on member servers and Windows NT Workstation/Windows 2000 Professional.
Set oIADsGroup = IADsCont.Create("localGroup", sGroup)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsContainer::Create method"
End If

'''''''''''''''''''''''''''''''''''''''
'Write the group to the computer's security database.
'''''''''''''''''''''''''''''''''''''''
oIADsGroup.SetInfo

'''''''''''''''''''''''''''''''''''''''
'Read the group that was just created
'and display its name and its members.
'''''''''''''''''''''''''''''''''''''''
strText = "The group " & oIADsGroup.Name & " was created on computer " & sComputer & "."
If (Err.Number <> 0) Then
    BailOnFailure Err.Number, "on IADsGroup::Name method"
End If
Set ListIADsMembers = oIADsGroup.members
If (Err.Number <> 0) Then
    BailOnFailure Err.Number, "on IADsGroup::members method"
End If
'Get the enumerate the members of the group from the members object
cmember = 0
For Each Member In ListIADsMembers
  If cmember = 0 Then
    strText = strText & vbCrLf & "       " & "Members:"
  End If
  strText = strText & vbCrLf & "       " & Member.Name & " (" & Member.Class & ")"
  cmember = cmember + 1
Next
If cmember = 0 Then
   strText = strText & vbCrLf & "       " & "No members"
End If

Call show_groups(strText, sComputer)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_groups(strText, strName)
    MsgBox strText, vbInformation, "Create group on " & strName
End Sub


Deleting Groups on Member Servers and Windows 2000 Professional

To delete a machine local group from a member server or computer running Windows® 2000 Professional

  1. Bind to the computer using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName , computer

      where sComputerName is the name of the computer that has the groups you want to access.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer and allows the WinNT provider's parser to skip some ambiguity resolution queries to determine what type of object you are binding to.

    3. Bind to the IADsContainer interface.
  2. Specify "group" as the class using IADsContainer::Delete to delete the group.

    Note that you do not need to call IADs::SetInfo to commit the change to the container. The IADsContainer::Delete call commits the deletion of the group directly to the directory.


Example Code for Deleting a Group on a Member Server or Windows NT Workstation/Windows 2000 Professional

[C++]

The following function deletes a group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

////////////////////////////////////////////////////////////////////////////////////////////////////
/*  DeleteADObject()   - Deletes the passed object by AdsPath

    Parameters

        LPOLESTR pwszAdsPath        -       AdsPath of object to delete

    Optional Parameters:

       LPOLESTR pwszUser            - User Name and Password, if the parameters are NOT passed,
       LPOLESTER pwszPassWord       - Binding will use ADsGetObject, if the parameters
                                    - Are specified, will use ADsOpenObject, passing user name and password

*/
HRESULT DeleteADObject(LPOLESTR pwszAdsPath, LPOLESTR  pwszUser,LPOLESTR  pwszPassWord)
{
    HRESULT             hr;
    BSTR                bsParentPath;
    IADs *              pIADsToDelete = NULL;
    IDirectoryObject *  pIDirObjectParent= NULL;
    VARIANT             vCNToDelete;
    WCHAR               pwszTemp[512];

    VariantInit(&vCNToDelete);
    OutputDebugString(pwszAdsPath);
    OutputDebugString(L"\r\n");

    // Bind to the object being deleted

    assert((pwszUser==NULL && pwszPassWord == NULL) || (pwszUser && pwszPassWord));

    // If a username and password are passed in, use ADsOpenObject()
    // otherwise use ADsGetObject()
    if (!pwszUser) // No user password passed, use ADsOpenObject
    {
        hr = ADsGetObject(  pwszAdsPath, IID_IADs,(void **)& pIADsToDelete);
    }
    else
    {
        hr = ADsOpenObject(pwszAdsPath, pwszUser, pwszPassWord,
                           ADS_SECURE_AUTHENTICATION,IID_IADs, (void**) & pIADsToDelete);
    }

    if (SUCCEEDED(hr))
    {
       // Get the parent path
        hr = pIADsToDelete->get_Parent(&bsParentPath);

        // Get the CN property for the object to delete
        hr = pIADsToDelete->Get(L"cn",&vCNToDelete);
        if (SUCCEEDED(hr))
        {
            // ************************************************************
            // Now bind to the parent
            // If a username and password are passed in, use ADsOpenObject()
            // otherwise use ADsGetObject()
            if (!pwszUser) // No user password passed, use ADsOpenObject
            {
                hr = ADsGetObject(  bsParentPath, IID_IDirectoryObject,(void **)& pIDirObjectParent);
            }
            else
            {
                hr = ADsOpenObject(bsParentPath, pwszUser, pwszPassWord,
                                   ADS_SECURE_AUTHENTICATION,IID_IDirectoryObject, (void**) & pIDirObjectParent);
            }
            if (SUCCEEDED(hr))
            {
                // Release the object to delete
                pIADsToDelete->Release();
                pIADsToDelete =NULL;

                // Put the CN property into a string beginning with CN=
                swprintf(pwszTemp,L"cn=%s\n",vCNToDelete.bstrVal);

                // Ask the parent to delete the child
                hr =pIDirObjectParent->DeleteDSObject(pwszTemp);
                // Release the Parent Object
                pIDirObjectParent->Release();
                pIDirObjectParent = NULL;
            }
        }
        SysFreeString(bsParentPath);
    }
    // If we have a IADsObject- we need to release it
    if ( pIADsToDelete)
    {
        // Release the object to delete
        pIADsToDelete->Release();
        pIADsToDelete =NULL;
    }

    VariantClear(&vCNToDelete);
 return hr;
}
[Visual Basic]

The following code deletes a group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

'Example: Deleting a local group on a member server or Windows NT Workstation/Windows 2000 Professional

'''''''''''''''''''''''''''''''''''''''
'Parse the arguments
'''''''''''''''''''''''''''''''''''''''
On Error Resume Next

Set oArgs = WScript.Arguments
If oArgs.Count < 2 Then
    sComputer = InputBox("This script deletes a group from a member server or workstation." & vbCrLf & vbCrLf &"Specify

the computer name:")
    sGroup = InputBox("Specify the group name:")
Else
    sComputer = oArgs.item(0)
    sGroup = oArgs.item(1)
End If

If sComputer = "" Then
    WScript.Echo "No computer name was specified. You must specify a computer name."
    WScript.Quit(1)
End If
If sGroup = "" Then
    WScript.Echo "No group name was specified. You must specify a group name."
    WScript.Quit(1)
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
Set cont= GetObject("WinNT://" & sComputer & ",computer")
If (Err.Number <> 0) Then
    BailOnFailure Err.Number, "on GetObject method"
End If

'''''''''''''''''''''''''''''''''''''''
'Delete the group
'''''''''''''''''''''''''''''''''''''''
'You do not need to specify localGroup, just group is sufficient.
Set oGroup = cont.Delete("group", sGroup)
If (Err.Number <> 0) Then
    BailOnFailure Err.Number, "on IADsContainer::Delete method"
End If

strText = "The group " & sGroup & " was deleted on computer " & sComputer & "."

Call show_groups(strText, sComputer)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_groups(strText, strName)
    MsgBox strText, vbInformation, "Create group on " & strName
End Sub


Adding Domain Groups to Machine Local Groups on Member Servers and Windows 2000 Professional

When a member server or a computer running Windows NT® Workstation or Windows® 2000 Professional is a member of a Windows 2000 domain, the groups or users in the domain can be used in the computer's local security database to grant rights to that group on that particular computer.

When managing groups on a Windows 2000 domain using ADSI, you use the LDAP provider. When managing groups on member servers and a computer running Windows NT Workstation/Windows 2000 Professional, you use the WinNT provider.

This means you need to bind at least once to each provider: 1) Bind to the LDAP provider to retrieve the ADsPath to the group or user you want to add to a group in the local database and 2) Bind to the WinNT provider to add that user or group to a machine local group.

Note that only machine local groups can be created on member servers and Windows 2000 Professional. However, those machine local groups can contain 1) Universal and global groups from the forest containing the domain that the computer is a member of or 2) Domain local groups from that computer's domain 3) Users from any domain in the forest.

To enumerate the machine local groups on a member server or computer running Windows 2000 Professional

  1. Bind to the group you want to add a member to using the following rules:
    1. Use an account that has sufficient rights to access that computer.
    2. Use the following binding string format using the WinNT provider, computer name, and an extra parameter to tell ADSI that it is binding to a computer:

      WinNT://sComputerName, computer

      where sComputerName is the name of the computer group you want to add a member to.

      In the binding string, the ",computer" parameter tells ADSI that it is binding to a computer. ADSI makes this information available to the WinNT provider's parser so that it can skip some ambiguity resolution queries to determine what type of object you are binding to. This can save the user a 5-20 second wait for the ambiguity to resolved.

    3. Bind to the IADsContainer interface of the computer.
    4. Use IADsContainer::GetObject to bind to the group object and specify "localGroup" as the class and the group name as the name of the object.
  2. Get the ADsPath of the user or group to add to the group. You should bind to the object using the WinNT provider, get an IADs pointer, and use the IADs::get_ADsPath property method to get the ADsPath of the object.

    If the user or group is in the current user's domain, use serverless binding and rootDSE to bind to the domain, get an IADs pointer, use the IADs::Get method to retrieve the dc attribute (this is the domain name), use the dc value to bind to the domain using a binding string with the following format:

    WinNT://sDomainName, domain

    where sDomainName is the name of the domain containing the user or group you want to add to the machine local group.

    Then, use IADsContainer::GetObject to bind to the user or group object and use the IADs::get_ADsPath method to get the ADsPath of the object.

  3. Add the user or group to the group, using the IADsGroup::Add method.


Example Code for Adding a Domain User or Group to a Matching Local Group

The following VBScript adds a domain user/group to a machine local group on a member server or a computer running Windows NT Workstation/Windows 2000 Professional:

' The following script adds a domain user/group to a machine local group on a member server or Windows NT Workstation/Windows 2000 Professional:

Dim IADsCont As IADsContainer
Dim ListIADsMembers As IADsMembers
Dim Member As IADs
Dim GroupIADsGroup As IADsGroup

sComputer = InputBox("This script adds a domain user/group to a local group on a member server or workstation." & vbCrLf & vbCrLf & "Specify the computer name of the group where you want to add the member:")
sGroup = InputBox("Specify the name of the group to which you want to add the member:")

If sComputer = "" Then
  MsgBox "No computer name was specified. You must specify a computer name."
  Exit Sub
End If
If sGroup = "" Then
  MsgBox "No group name was specified. You must specify a group name."
  Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the computer
'''''''''''''''''''''''''''''''''''''''
'
Set IADsCont = GetObject("WinNT://" & sComputer & ",computer")
Set GroupIADsGroup = IADsCont.GetObject("LocalGroup", sGroup)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on GetObject method"
End If

strText = ""
cmember = 0

'''''''''''''''''''''''''''''''''''''''
'Get display group name and its members
'''''''''''''''''''''''''''''''''''''''
    'Get the name
    strText = strText & vbCrLf & "The group " & GroupIADsGroup.Name & " currently has the following members:"
    'Get the members object
    Set ListIADsMembers = GroupIADsGroup.members
    If (Err.Number <> 0) Then
       BailOnFailure Err.Number, "on IADsGroup::members method"
    End If
    'Enumerate the members of the group from the members object
    For Each Member In ListIADsMembers
      strText = strText & vbCrLf & "       " & Member.Name & " (" & Member.Class & ")"
      cmember = cmember + 1
    Next
    If cmember = 0 Then
      strText = strText & vbCrLf & "       " & "No members"
    End If
Call show_groups(strText, sComputer)

sNewMember = InputBox("Specify the ADsPath of the domain user or group to add:")
If sNewMember = "" Then
  MsgBox "No member was specified. You must specify a member to add."
  Exit Sub
End If

'''''''''''''''''''''''''''''''''''''''
'Bind to the member you want to add
'''''''''''''''''''''''''''''''''''''''
Set NewMember = GetObject(sNewMember)
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "Could not bind to user or group to add."
End If
'Call show_groups(member.ADsPath, sComputer)

GroupIADsGroup.Add (NewMember.ADsPath)
'group.Add("WinNT://timto1923dom/yyyy")
'group.Add("WinNT://redmond /domain users")
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "Could not add member to group."
End If

'''''''''''''''''''''''''''''''''''''''
'Confirm success and display group name and its members
'''''''''''''''''''''''''''''''''''''''
strText = "Member was successfully added."
strText = strText & vbCrLf & "The group " & GroupIADsGroup.Name & " now has the following members:"
'Get the members object
Set ListIADsMembers = GroupIADsGroup.members
If (Err.Number <> 0) Then
   BailOnFailure Err.Number, "on IADsGroup::members method"
End If
'Enumerate the members of the group from the members object
For Each Member In ListIADsMembers
  strText = strText & vbCrLf & "       " & Member.Name & " (" & Member.Class & ")"
  cmember = cmember + 1
Next
If cmember = 0 Then
  strText = strText & vbCrLf & "       " & "No members"
End If
Call show_groups(strText, sComputer)

'''''''''''''''''''''''''''''''''''''''
'Display subroutines
'''''''''''''''''''''''''''''''''''''''
Sub show_groups(strText, strName)
    MsgBox strText, vbInformation, "Groups on " & strName
End Sub

Sub BailOnFailure(ErrNum, ErrText)    strText = "Error 0x" & Hex(ErrNum) & " " & ErrText
    MsgBox strText, vbInformation, "ADSI Error"
    WScript.Quit
End Sub


What Application and Service Developers Need to Know About Groups

When designing an application or service, you may be tempted to use groups to delegate administration or access control to the application or service as a whole or some portion of it. However, creating dependencies on hard-coded groups can cause serious problems if the group, for example, is deleted or moved. In addition, creating groups that have a limited use (that is, the groups only apply when running the application or accessing the service) unnecessarily adds the SIDs for the groups to the access tokens of the users who are members of those groups. This can slow logon performance for those users (access token is created at logon time) as well as access checks when accessing resources.

If you want to control access to an application or service, you should use the extended rights feature of access control in Windows® 2000:

  1. Create an extended right that defines the type of access to the application or service.
  2. Create an object that represents the application, service, or resource in the directory.
  3. Add object ACEs to the DACL in the object's security descriptor to allow or deny users or groups the extended right on that object.
  4. Use the AccessCheckByTypeResultList function in the application or service to check the user's access on the object.
  5. Based on the result of the access check on the object, the application or service can allow or deny the user access to the application or service.

When you use extended rights, you do not need to explicitly create groups in order to control access. If you need to assign the extended rights for your application or service to a group (for example, if you want to assign administrative rights by default to a group of users), you should try to use the appropriate built-in group, if possible.

If you do create your own groups, you should protect the group using its ACL (delete or modify or restrict to modify members only), use global groups if they are used for access control on DS objects, use universal groups only if necessary (member info required globally using global catalog; group can contain any user/group) and if you do use them, place global groups in the universal group and add/remove users from the global group. Avoid excess change to universal groups for replication efficiency.


Service Publication

Services advertise themselves using objects stored in Active Directory™. Clients query the directory to locate the services of interest. This general subject area is called client-server rendezvous. This chapter defines service related objects and explains how they are used for client-server rendezvous.

This chapter covers publishing a service in Active Directory. This includes using the RPC name service (RpcNs) and Windows® sockets registration and resolution (RnR), creating and using service connection points (SCPs), and using the COM+ class store. Services use one or more of these mechanisms to advertise themselves. Clients find an object advertised by the service, then use binding information in this object to connect to the service.

Note that this chapter does not contain information about security issues for client-server rendezvous.

This chapter covers the following topics:

  • Overview of service publication
  • Directory infrastructure for service publication
  • Publishing a service


Overview of Service Publication

A service is an application that makes data or operations available to network clients. Often, a service is implemented as a formal Win32-based Service, but this is not required.

Service publication is the act of creating and maintaining information about one or more instances of a given service so that network clients can access the service. Publishing a service in Active Directory enables clients and administrators to move from a machine-centric view of the distributed system to a service-centric view.

In Windows NT® 4.0 and earlier, a distributed system was a group of computers running various services. To access a service, an application needed to know which computers offered the service.

In Windows 2000, services publish their existence using objects in Active Directory. The objects contain binding information that applications use to connect to instances of the service. To access a service, an application does not need to know about specific computers: the objects in Active Directory include this information. An application queries Active Directory for an object representing a service (called a connection point object) and uses the binding information from the object to connect to the service.

The following tables shows examples of bindings.

Service | Binding
File Service | UNC Name for a share. Example: \\MyServer\MyshareName
Web Service | URL. Example: http://www.arcadiabay.com
RPC Service | RPC binding: special encoded information used to connect to the RPC server. RPC bindings can be converted to and from strings with the RPC APIs. Example: ncacn_ip_tcp:server.microsoft.com

In a distributed system the computers are engines, and the interesting entities are the services that are available. From the user's perspective, the identity of the computer that provides a particular service is not important. What is important is accessing the service itself.

This is also the case with service management. The administrator of a given DNS zone is not interested in the computers running the DNS service; the administrator wants to administer DNS. There will likely be multiple instances of the DNS service, one of which (in classic DNS) will be authoritative. The computers that support the DNS service are not important to the DNS administrator. What is important is managing the service as a single distributed resource — not as individual processes running on different computers.


Directory Infrastructure for Service Publication

The objects stored in Active Directory are arranged in a hierarchy. Active Directory is shipped with a pre-configured object hierarchy that contains the information required to install and run Windows 2000 and the directory service itself. The default structure includes containers intended specifically for use in service publication, as shown in the following figure.

Part of the domain hierarchy appears on the left. The domain hierarchy holds all of the objects (for example, users, computers, printers, and so on) related to a particular domain, and is replicated to all domain controllers for that domain. The configuration hierarchy appears on the right. The configuration holds all of the objects related to a complete forest of domains. The configuration hierarchy is replicated to every domain controller in the forest of domains.


Where to Publish

Guiding Principles:

  • A service should publish its information in the domain hierarchy, never in the configuration hierarchy.
  • A service should create service-specific objects in the same domain as the computer running the service, and in locations that are convenient for the administration of the service and maintenance of those objects. Usually, the best location is the computer object where the service is installed.

If a client is able to access a computer running a service, that client should be able to access a replica of the domain containing that computer. Therefore, it is sufficient for a service to publish information to the domain containing the computer running the service. Publishing service information to the configuration hierarchy does not make the service more highly available or easier to access. It does, however, cause extra replication traffic. Do not publish service information to the configuration hierarchy.

Clients find service objects by querying the directory. The client can scope the query to its domain or can search the entire forest using the global catalog. In either case, the specific location of the service object is irrelevant to the client. Therefore, the client does not influence where to publish service objects. Instead, the key consideration is ease of service administration.

Service-related objects "naturally" occur in three places in a domain hierarchy:

  • Computer
  • Organizational-Unit
  • System container, or one of its children

Note that in Active Directory (and many other directories) any object may be named as a valid parent, making it a container. This allows the directory to have an intuitive structure. For example, objects that relate to exactly one computer should appear as children of that computer, where an administrator will expect to find them.


Computer Object

Guiding Principle: A service should create service-specific objects as children of the computer object for the computer where the service is installed.

For ease of installation, many services will want a "default" location in which to create their objects. (As we'll see later, most services will want to allow their objects to be moved after creation, by an administrator.) The computer object for the computer where the service is installed provides a convenient and intuitive container to hold these objects. A process running on a given computer that is a domain member can always find the computer object in the directory by calling the GetComputerObjectName function in the Win32 API to get the distinguished name (DN) of the computer object that represents the local machine in the directory. Note that only machines that are domain members have valid computer objects in the directory.

The following program gets the distinguished name of the local computer.

/*
**     Get the DN for the computer object of the local computer
**     in the directory service, bind to the object using ADSI,
**     and read the DNS name.
**     Additional libraries required -for SSPI: secur32.lib
**                                   -for ADSI: activeds.lib
**                                              adsiid.lib
**
*/

#include <windows.h>
#include <wchar.h>
#include <objbase.h>
//#include <stdio.h>
#include <activeds.h>

//Required by Security.h
#define SECURITY_WIN32
#include <security.h>

void
main()
{
    BOOLEAN br;
    WCHAR ComputerDN[MAX_PATH];
    WCHAR ComputerAdsPath[MAX_PATH+7]; //Add space for "LDAP://"
    ULONG ulen;

    //For ADSI
    HRESULT hr;
    IADs    *pObj;
    VARIANT varHost;

    CoInitialize(NULL);
    //Get the DN of the local computer.
    ulen = sizeof(ComputerDN);
    br=GetComputerObjectName(NameFullyQualifiedDN,  //Name form from SSPI.H
                         ComputerDN,
                         &ulen);
   if (br)
   {
        wprintf(L"DN for the local computer: %s\n",ComputerDN);
      wcscat(ComputerAdsPath,L"LDAP://");
      wcscat(ComputerAdsPath,ComputerDN);
      hr = ADsGetObject(ComputerAdsPath,
                    IID_IADs,
                    (void**)&pObj);
      if (SUCCEEDED(hr))
      {
         hr = pObj->Get(L"dNSHostName",&varHost);
         if (SUCCEEDED(hr))
         {
            wprintf(L"DNS Name of local computer:%s\n",varHost.bstrVal);
         }
      }

      //Clean up
      VariantClear(&varHost);
      pObj->Release();
   }
   else
      wprintf(L"GetComputerObjectName failed");

    CoUninitialize();
}


Organizational Unit Container Hierarchy

The directory allows administrators to create a container hierarchy that meets the needs of their organization. The object class of choice for building these hierarchies is organizationalUnit, which is a general purpose container that can be used to group most other object classes together for purposes of administration. Many user sites will want to group service-specific objects together for the same reason. Active Directory enables an administrator to set permissions on all of the objects in a container or subtree with a single access-control list (ACL). The permissions model in Windows 2000 is both rich and flexible, allowing administrators to apply very fine-grained access control to objects in the directory and to delegate administrative authority to other users. Service-specific objects should be valid in organizationalUnit containers, so administrators can group them for purposes of administration. Note that all of the standard service-related object classes are valid chidren of the organizationalUnit class.

This means that a service should not require its objects to reside at a fixed location in the directory. Rather than accessing its object by location, a service should access its objects by querying for them. A reliable and high-performance way to query for a service object is to search by objectGuid. To enable such searches, a service would read the objectGuid from each object it creates, and store the objectGuid in a local database, file, or registry. In some cases, a service may be able to query for its service objects without using their objectGuids. Active Directory provides search capabilities so a service instance can find the service-specific objects that apply to it, regardless of their location in the directory.

For sample code that creates a service object, stores its objectGUID, and finds the service object using the objectGUID, see Publishing with Service Connection Points.

If a service creates an object under the computer object as recommended above, it must not rely upon the invariance of the distinguished name (DN) of the new object. An administrator must have the freedom to move the computer object, changing the DN of the computer object and its descendants in the hierarchy.


Users and Computers Containers

The Windows 2000 upgrade process places users, groups, and machine accounts in the users and computers containers. User and group objects are stored in the users container; computer objects in the computers container. Down-level tools and API (such as the Net API) that create users, groups, and computer accounts create them in these containers.

Services should not publish objects directly in these containers , or create new containers in these containers.

However, services can publish objects as children of a computer object, whether or not the computer object is stored in the computers container.


System Container

The system container holds per-domain operational information. This includes the default local security policy, file link tracking, network meetings, and containers for RPC and Winsock connection points. The system container is hidden by default and provides a convenient place for storing objects that are of interest to administrators, but not to end users.

Services that use Winsock RnR or RpcNs APIs to advertise themselves will automatically create the proper objects in the WinsockServices and RpcServices containers. Do not explicitly create objects in these containers. Doing so will cause no direct harm, but it will be confusing for administrators.

Services that create service-specific objects in the system container must do the following:

  1. Create a sub-container of object class container, which is an immediate child of the system container, with a name that clearly identifies the container as pertaining to the service.
  2. Publish the service-related objects in this container. For example, NetMeeting uses the meetings container to publish network meeting objects. When multiple products from a single vendor publish objects in this manner, the containers for each service should be created as children of a container (an object of class container) for the vendor whose name clearly identifies that vendor. The vendor-specific container must be created as a child of the system container.

You would publish in a container beneath the system container in a case where your service is not strongly tied to a single computer. NetMeeting is a good example.


Connection Points

A connection point object represents one or more instances of a service available in the network. The connectionPoint class is the base class from which all objects representing "connectable resources" are derived. These include Volume, Print-Queue, RPC objects, Windows Sockets objects, and the serviceConnectionPoint object.

Connection Point Classes

The Active Directory schema defines a variety of object classes for use in service publication. All of these objects are derived from a single class: Connection-Point. The class hierarchy is shown below.

PrintQueue and Volume are the standard connection points used by print and file services respectively.

RPCEntry and its descendants are used by the RPC Name Service (Ns), which is accessed via the RpcNs* functions in the Win32 API.

ServiceInstance is used by the Windows Sockets Registration and Resolution ("RnR") name service, which is accessed via the Windows Sockets WSA* APIs.

Services that currently advertise themselves through the RPC Name Service or through Windows Sockets RnR will automatically be published in the directory using these object classes.

ServiceConnectionPoint is used by services that need to explicitly publish them selves in the directory rather than use an existing abstraction, such as RpcNs or RnR.

A service that uses ServiceConnectionPoint should provide an abstraction layer to hide service location details from client applications. This abstraction (which can be implemented as a DLL or as part of the client application) queries Active Directory for a connection point object representing the service requested by the client application and uses the binding information from that object to connect the client application to the service.

COM-based services do not use connectionPoint objects to advertise themselves. These services are published in the class store. The Windows 2000 class store is a directory-based repository for all applications, interfaces, and APIs that provide for application publishing and assigning.

COM-based services provide an "application proxy" in the form of an MSI installation package. This MSI file contains the server name to be used and other "glue" like proxy/stubs and type libraries needed for marshalling. The Component Services snap-in automatically generates these application proxies for COM+ Server applications.

The "Application Proxies" are published into policy objects in Active Directory using the Group Policy Editor. No special intervention is required in the client application. The machine/user account on the client machine needs to be in an OU that is configured to use the policy object in which the application proxies are published. The COM Binder will automatically locate the server via the directory when the client instantiates the object(s) in question.


Publishing a Service

The following are uiding principles for publishing a service:

  1. Create connection point objects during service installation so the procedure can be performed by a user with sufficient privilieges.
  2. Restrict the service to updating existing connection point objects at run time.

Regardless of the mechanism chosen for publishing a service you must understand and allow for access restrictions on the directory in your installation procedure and at run time. The RPC Name Service (RpcNs) and Windows Sockets Registration and Resolution (RnR) APIs create connection points in the RpcServices and WinsockServices container respectively. These are found in the System container in each Domain. ServiceConnectionPoint objects should be created as children of the Computer object for the computer where the service is installed.

To create an object (of any kind) the process performing the operation must have create child permission for the object class to be created in the container where the object will be created. To remove an object the process performing the operation must have delete child permissions for the object class to be deleted on the container holding the object, or have delete permission on the object itself. To update a connection point the process performing the operation must have write access to the properties to be updated on the object in question.

All of these rights are unlikely to be available to a service account or machine account create objects during installation; the installation can be performed by an administrator or other suitably privileged user. As part of the installation set the protection on any connection point object to allow update by the service at run time. This allows the service to run with a minimally privileged account and still maintain its connection point.


Publishing with the RPC Name Service (RpcNs)

RPC services publish themselves in a namespace using the RPC name service (RPCNs) APIs. The RPCNs APIs in Windows 2000 publish the RPC entries in Active Directory. Services create RPC bindings and publish them in the namespace as named RPC Server entries with attributes including the unique interface ID, a GUID that is known to clients. Clients can then search for RPC Servers offering the desired interface, import the binding, and connect to the server.


Example Code for Publishing an RPC Service

The following code fragment publishes an RPC service:

    status = RpcServerUseProtseq((TCHAR *)TEXT("ncacn_ip_tcp"),
                                 RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
                                 NULL);

    //Register our interface with the RPC run time.

    status = RpcServerRegisterIf(RpcExample_v1_0_s_ifspec,NULL,NULL);

    //Collect the bindings provided by the RPC run time.

    status = RpcServerInqBindings(&pBindingVec);

    //Clear the endpoints out of the binding handles.
    //This forces the clients through the Endpoint Mapper and
    //reduces the volatility of the namespace entry for this
    //service.

    for (i=0; i < pBindingVec->Count; ++i) {
        RpcBindingReset(pBindingVec->BindingH[i]);
    }

    //Create the entry in the name service for this service.

    status = RpcNsBindingExport(RPC_C_NS_SYNTAX_DEFAULT,
                                (TCHAR *)&szEntryName,
                                RpcExample_v1_0_s_ifspec,
                                pBindingVec,
                                NULL);
Example 1: Registering with the Endpoint Mapper and starting the service

    //Use TCP/IP as the protocol: max concurrent users 64,
    //no NT Security Descriptor.

    status = RpcServerUseProtseq((TCHAR *)TEXT("ncacn_ip_tcp"),
                                 RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
                                 NULL);

    //Register our interface with the RPC run time.

    status = RpcServerRegisterIf(RpcExample_v1_0_s_ifspec,NULL,NULL);

    //Collect the bindings provided by the RPC run time.

    status = RpcServerInqBindings(&pBindingVec);

    //Register the interface with the Endpoint Mapper.
    //There are no Object UUIDs or Annotation string in this example.

    status = RpcEpRegister(RpcExample_v1_0_s_ifspec,
                            pBindingVec,NULL,NULL);

    //Start the server. When RpcServerListen returns, the server is
    //shutting down.

    if (status != RPC_S_OK)
        return;

    //Handle calls from clients until we are shut down.

status = RpcServerListen(1,
                             RPC_C_LISTEN_MAX_CALLS_DEFAULT,
                             0);

    //Remove this service from the endpoint map. We need to get a
    //new binding vector since the old one has the endpoints removed
    //and does not match what we registered.
    //
    //RpcBindingVectorFree(&pBindingVec);
    //RpcServerInqBindings(&pBindingVec);
    status = RpcEpUnregister(RpcExample_v1_0_s_ifspec,pBindingVec,
                             NULL);


Example Code for an RPC Client Locating a Server

The following code fragment locates a server with the example RPC service:

    //Begin the import. We explicitly start with the a known
    //entry, "ExampleRpcServiceEntry". The interface we are
    //looking for is defined by RpcExample_v1_0_s_ifspec, which
    //is generated by MIDL.

    status = RpcNsBindingImportBegin(RPC_C_NS_SYNTAX_DCE,
                                     (TCHAR *)&szEntryName,
                                     RpcExample_v1_0_c_ifspec,
                                     NULL,
                                     &hNs);

    //Abandon if we cannot find anything or get any errors.
    if (status != RPC_S_OK)
        return;

    //Now acquire the first available binding handle. In this
    //example, implicit handles are used: the implicit handle
    //"RpcExample_IfHandle" is generated by MIDL and defined in the
    //MIDL-generated include file RpcExample.H.

    //Loop through the bindings and try handles
    //until one works.

    while (status != RPC_S_NO_MORE_BINDINGS) {
        status = RpcNsBindingImportNext(hNs,
                                        &RpcExample_IfHandle);

        if (status != RPC_S_OK)
            continue;

        //Convert the binding to a string and display it to
        //the user/

        RpcBindingToStringBinding(RpcExample_IfHandle,&pszBind);
        _tprintf(TEXT("String Binding:%s\n"),pszBind);

        //Extract the service name, the host, in this case).

        ilen=_tcscspn((const TCHAR *)pszBind,TEXT(":"));
        _tcscpy(szServiceInstance,(const TCHAR *)(pszBind+ilen+1));

        RpcStringFree(&pszBind);

        //Make the RPC - Call the Server, then shut it down.

        RpcTryExcept {
            bResult = ServerProc((TCHAR *)TEXT("Text sent to server"));
            if (bResult)
                _tprintf(TEXT("ServerProc: Server accepted call\n"));
            else
                _tprintf(TEXT("ServerProc: Server rejected call\n"));

            bResult = Shutdown();
            if (bResult)
                _tprintf(TEXT("Shutdown: Server accepted call\n"));
            else
                _tprintf(TEXT("Shutdown: Server rejected call\n"));
            status  = RPC_S_NO_MORE_BINDINGS; //shut off the loop
        }
        RpcExcept(1) {
            ulCode = RpcExceptionCode();
            _tprintf(TEXT("RPC exception 0x%lx = %ld\n"), ulCode, ulCode);
        }
        RpcEndExcept;

        //Free the imported binding.

        RpcBindingFree(&RpcExample_IfHandle);
    }

    //Discard the Import handle, we don't need it any more.

    status = RpcNsBindingImportDone(&hNs);


Publishing with Windows Sockets Registration and Resolution (RnR)

Windows Sockets services can use the registration and resolution (RnR) APIs to publish services and look up services so published. RnR publication occurs in two steps. The first step installs a "Service Class" that associates a GUID with a name for the service. The service class can hold service-specific configuration information. Services can then publish themselves as instances of the service class. Once published, clients can query the directory service for instances of a given class using the RnR APIs and select an instance to bind to. When a class is no longer useful, it can be removed.


Example Code for Installing an RnR Service Class

The following program installs an RnR Service Class.

#include <winsock2.h>
#include <stdio.h>

#define BUFFSIZE 200

//{A9033BC1-ECA4-11cf-A054-00AA006C33ED}
static GUID SVCID_EXAMPLE_SERVICE =
{ 0xa9033bc1, 0xeca4, 0x11cf, { 0xa0, 0x54, 0x0, 0xaa, 0x0, 0x6c, 0x33, 0xed } };

HRESULT
main(void)
{

//Data structures for initializing Winsock.

WSADATA wsData;
WORD    wVer = MAKEWORD(2,2);

//Data structures for defining and installing a
//Winsock Service Class.

WSASERVICECLASSINFO    servinfo;
WSANSCLASSINFO        classinfo[2];

//Values for initializing the data structures.

WCHAR    wszServiceName[]=L"ExampleService";
WCHAR    wszParam1[]    =L"MaxConnections";
WCHAR    wszParam2[]    =L"Timeout";
DWORD    dwConn        = 64;
DWORD    dwTmo        = 1500;

//Miscellaneous variables for status etc.

DWORD   iLen;
WCHAR   buf[BUFFSIZE];
INT    status;

//Begin: Init Winsock2

status = WSAStartup(wVer,&wsData);
if (status != NO_ERROR)
    return -1;

//Set up the Service Class data structure.

servinfo.lpServiceClassId           = &SVCID_EXAMPLE_SERVICE;
servinfo.lpszServiceClassName    = wszServiceName;
servinfo.dwCount                = 2;
servinfo.lpClassInfos            = classinfo;

//This service has two items of class-specific data;
//set these up in the classinfo array.

classinfo[0].lpszName       = wszParam1;
classinfo[0].dwNameSpace    = NS_NTDS;
classinfo[0].dwValueType    = REG_DWORD;
classinfo[0].dwValueSize    = sizeof(DWORD);
classinfo[0].lpValue        = &dwConn;

classinfo[1].lpszName       = wszParam2;
classinfo[1].dwNameSpace    = NS_NTDS;
classinfo[1].dwValueType    = REG_DWORD;
classinfo[1].dwValueSize    = sizeof(DWORD);
classinfo[1].lpValue        = &dwTmo;

status = WSAInstallServiceClass(&servinfo);
if (status != NO_ERROR)
    printf("Install failed with status %d\n",WSAGetLastError());

//Read the class name back.

iLen = BUFFSIZE;

//Status = WSAGetServiceClassNameByClassId(&SVCID_EXAMPLE_SERVICE,
//                                         buf,&iLen);
//if (status != NO_ERROR) {
//        printf("GetServiceClassName failed with status %d\n",
//               WSAGetLastError());
//}

//Free resources and exit.

WSACleanup();
return 0;

}


Example Code for Implementing a Winsock Server with an RnR Publication

The following program implements the example Winsock server with RnR publication. Note that the section following the current section contains the code for the serverRegister and serverUnregister functions.

/*
** Simple Winsock Server
*/
#include <winsock2.h>
#include <stdio.h>

INT serverRegister(SOCKADDR *);
INT serverUnregister(SOCKADDR *);

HRESULT
main(void)
{

    //Data structures for initializing Winsock.

    WSADATA wsData;
    WORD    wVer = MAKEWORD(2,2);

    //Data structures for setting up communications.

    SOCKET        s, newsock;
    SOCKADDR    sa;
    struct hostent    *he;
    char    szName[255];

    struct sockaddr_in sa_in;

    INT            ilen;
    ULONG        icmd;
    ULONG        ulLen;

    //Miscellaneous variables

    INT status;
    WCHAR    wszRecvBuf[100];

    memset(wszRecvBuf,0,sizeof(wszRecvBuf));

    //Begin: Init Winsock2

    status = WSAStartup(wVer,&wsData);
    if (status != NO_ERROR)
        return -1;

    //Create the socket.

    s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if (s == INVALID_SOCKET) {
        printf("Failed to create socket: %d\n",WSAGetLastError());
        WSACleanup();
        return -1;
    }

    //Disable non-blocking IO for purposes of this example.

    icmd = 0;
    status = ioctlsocket(s,FIONBIO,&icmd);

    //Bind the socket to a dynamically assigned port.

    sa.sa_family=AF_INET;
    memset(sa.sa_data,0,sizeof(sa.sa_data));

    status = bind(s,&sa,sizeof(sa));

    //Note that we need to convert the port to the local
    //host byte order.

    ilen = sizeof(sa_in);
    status = getsockname(s,(struct sockaddr *)&sa_in,&ilen);
    if (status == NO_ERROR) {
        printf("Server: Bound to port %d\n",ntohs(sa_in.sin_port));
    }
    //Figure out our net address to fill in for registering ourselves
    //in the directory service. Explicitly call the ANSI text version
    //because gethostbyname does not understand UNICODE.

    ilen = sizeof(szName);
    GetComputerNameA(szName,&ulLen);
    he = gethostbyname(szName);

    //Put the address in the SOCKADDR struct. This is not
    //as difficult as it looks.

    sa_in.sin_addr.S_un.S_addr = *((long *)(he->h_addr));

    //Listen for connections. SOMAXCONN tells the provider to queue
    //a "reasonable" number of connections.

    status = listen(s,SOMAXCONN);
    if (status != NO_ERROR) {
        printf("Failed to set socket listening: %d\n",
                WSAGetLastError());
        WSACleanup();
        return -1;
    }

    //Register this instance with RnR.

    status = serverRegister((SOCKADDR *)&sa_in);
    if (status != NO_ERROR) {
        printf("Failed to register instance: %d\n",WSAGetLastError());
        WSACleanup();
        return -1;
    }
    printf("Server: Registered instance in the directory service\n");

    //Block waiting for a connection. This example is single-threaded
    //for simplicity. In a real service, spin off
    //one or more threads here to call AcceptEx here and process the
    //connections through a completion port.

    ilen = sizeof(sa);
    newsock = accept(s,&sa,&ilen);
    if (newsock == INVALID_SOCKET)
    {
        printf("Failed to create socket: %d\n",WSAGetLastError());
        status = serverUnregister((SOCKADDR *)&sa_in);
        WSACleanup();
        return -1;
    }

    printf("Server: There is a client connection\n");

    //Receive a message from the client and shut down.

    status = recv(newsock,(char *)wszRecvBuf,sizeof(wszRecvBuf),0);
    if (status > 0)
            printf("Server: Received: %S\n",wszRecvBuf);

    //Unregister ourselves
    printf("Unregistering and shutting down.\n");
    status = serverUnregister((SOCKADDR *)&sa_in);

    WSACleanup();
    return 0;
}


Example Code for Publishing the RnR Connection Point

The following function is used by the example Winsock server above to register the RnR connection point for the service.

/*
** Simple Winsock Server
*/
#include <winsock2.h>
#include <stdio.h>

//{A9033BC1-ECA4-11cf-A054-00AA006C33ED}
static GUID SVCID_EXAMPLE_SERVICE =
{ 0xa9033bc1, 0xeca4, 0x11cf, { 0xa0, 0x54, 0x0, 0xaa, 0x0, 0x6c, 0x33, 0xed } };

static WCHAR    ServiceInstanceName[] = L"Example Service Instance";
static WCHAR    ServiceInstanceComment[] = L"ExampleService instance registered in the directory service through RnR";

INT
serverRegister(SOCKADDR * sa)
{
    DWORD               ret;
    WSAVERSION          Version;
    WSAQUERYSET         QuerySet;
    CSADDR_INFO         CSAddrInfo[1];
    SOCKADDR            sa_local;

    memset(&QuerySet,0,sizeof(QuerySet));
    memset(&CSAddrInfo,0,sizeof(CSAddrInfo));
    memset(&sa_local,0,sizeof(SOCKADDR));
    sa_local.sa_family = AF_INET;

    //Build the CSAddrInfo structure to contain address
    //information. This is what clients will use to make a connection.
    //
    //Note that we zeroed out the LocalAddr because we are using
    //dynamically assigned port numbers.
    //
    CSAddrInfo[0].LocalAddr.iSockaddrLength = sizeof( SOCKADDR );
    CSAddrInfo[0].LocalAddr.lpSockaddr = &sa_local;
    CSAddrInfo[0].RemoteAddr.iSockaddrLength = sizeof( SOCKADDR );
    CSAddrInfo[0].RemoteAddr.lpSockaddr = sa;
    CSAddrInfo[0].iSocketType = SOCK_STREAM;
    CSAddrInfo[0].iProtocol = PF_INET;

    QuerySet.dwSize = sizeof( WSAQUERYSET );
    QuerySet.lpServiceClassId = &SVCID_EXAMPLE_SERVICE;
    QuerySet.lpszServiceInstanceName = ServiceInstanceName;
    QuerySet.lpszComment = ServiceInstanceComment;
    QuerySet.lpVersion = &Version;
    QuerySet.lpVersion->dwVersion = 2;
    QuerySet.lpVersion->ecHow = COMP_NOTLESS;
    QuerySet.dwNameSpace = NS_NTDS;
    QuerySet.dwNumberOfCsAddrs = 1;
    QuerySet.lpcsaBuffer = CSAddrInfo;

    ret = WSASetService( &QuerySet,
                         RNRSERVICE_REGISTER,
                         SERVICE_MULTIPLE );

    return( ret );

}


Example Code for Removing the RnR Connection Point

The following function is used by the example Winsock server above to unregister the RnR connection point for the service.

/*
** Simple Winsock Server
*/
#include <winsock2.h>
#include <stdio.h>

//{A9033BC1-ECA4-11cf-A054-00AA006C33ED}
static GUID SVCID_EXAMPLE_SERVICE =
{ 0xa9033bc1, 0xeca4, 0x11cf, { 0xa0, 0x54, 0x0, 0xaa, 0x0, 0x6c, 0x33, 0xed } };

static WCHAR    ServiceInstanceName[] = L"Example Service Instance";
static WCHAR    ServiceInstanceComment[] = L"ExampleService instance registered in the directory service through RnR";

INT
serverUnregister(SOCKADDR * sa)
{
    DWORD               ret;
    WSAVERSION          Version;
    WSAQUERYSET         QuerySet;
    CSADDR_INFO         CSAddrInfo[1];
    SOCKADDR            sa_local;

    memset(&QuerySet,0,sizeof(QuerySet));
    memset(&CSAddrInfo,0,sizeof(CSAddrInfo));
    memset(&sa_local,0,sizeof(SOCKADDR));
    sa_local.sa_family = AF_INET;

    //Build the CSAddrInfo structure to contain address
    //information. This is what clients will use to make a connection
    //
    //Note that the LocalAddr is zeroed out because
    //dynamically assigned port numbers are used.
    //
    CSAddrInfo[0].LocalAddr.iSockaddrLength = sizeof( SOCKADDR );
    CSAddrInfo[0].LocalAddr.lpSockaddr = &sa_local;
    CSAddrInfo[0].RemoteAddr.iSockaddrLength = sizeof( SOCKADDR );
    CSAddrInfo[0].RemoteAddr.lpSockaddr = sa;
    CSAddrInfo[0].iSocketType = SOCK_STREAM;
    CSAddrInfo[0].iProtocol = PF_INET;

    QuerySet.dwSize = sizeof( WSAQUERYSET );
    QuerySet.lpServiceClassId = &SVCID_EXAMPLE_SERVICE;
    QuerySet.lpszServiceInstanceName = ServiceInstanceName;
    QuerySet.lpszComment = ServiceInstanceComment;
    QuerySet.lpVersion = &Version;
    QuerySet.lpVersion->dwVersion = 2;
    QuerySet.lpVersion->ecHow = COMP_NOTLESS;
    QuerySet.dwNameSpace = NS_NTDS;
    QuerySet.dwNumberOfCsAddrs = 1;
    QuerySet.lpcsaBuffer = CSAddrInfo;

    ret = WSASetService( &QuerySet,
                         RNRSERVICE_DEREGISTER,
                         SERVICE_MULTIPLE );

    return( ret );

}


Example Code for a Winsock Client Locating a Server Using an RnR Query

The following program locates the example Winsock server and connects to it.

/*
** Simple Winsock Client
*/
#include <winsock2.h>
#include <stdio.h>

#define BUFSIZE 3000

/ {A9033BC1-ECA4-11cf-A054-00AA006C33ED}
static GUID SVCID_EXAMPLE_SERVICE =
{ 0xa9033bc1, 0xeca4, 0x11cf, { 0xa0, 0x54, 0x0, 0xaa, 0x0, 0x6c, 0x33, 0xed } };

static WCHAR    ServiceInstanceName[] = L"Example Service Instance";
static WCHAR    wszClientMessage[] = L"The directory service.";

HRESULT
main(void)
{

    //Data structures for initializing Winsock

    WSADATA wsData;
    WORD    wVer = MAKEWORD(2,2);

    //Data structures for searching

    HANDLE                 hQ;
    WSAQUERYSET            QuerySet;
    PWSAQUERYSET           ResultSet;
    DWORD                  dwResultSize;
    WCHAR                  wBuffer[BUFSIZE];

    //Data structures for setting up communications

    SOCKET        s;

    //Miscellaneous variables

    INT status;
    DWORD iAddr;
    BOOL bConnected = FALSE;

    //Clear out the data structures

    ResultSet = (PWSAQUERYSET)wBuffer;
    memset(&QuerySet,0,sizeof(QuerySet));
    memset(ResultSet,0,sizeof(*ResultSet));

    //Begin: Init Winsock2

    status = WSAStartup(wVer,&wsData);
    if (status != NO_ERROR)
        return -1;

    //Create the socket

    s = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if (s == INVALID_SOCKET)
    {
        printf("Failed to create socket: %d\n",WSAGetLastError());
        WSACleanup();
        return -1;
    }

    //Set up the search

    QuerySet.dwSize = sizeof(QuerySet);
    QuerySet.lpServiceClassId = &SVCID_EXAMPLE_SERVICE;
    QuerySet.dwNameSpace = NS_NTDS;
    QuerySet.lpszServiceInstanceName = ServiceInstanceName;

    printf("Client: querying the directory service for service instance...\n");

    //Look up the Server to use

    status = WSALookupServiceBegin(&QuerySet,
                                    LUP_RETURN_NAME |
                                    LUP_RETURN_ADDR |
                                    LUP_RETURN_TYPE,
                                    &hQ);
    if (status != NO_ERROR)
        {
        if (status == WSASERVICE_NOT_FOUND)
            {
            printf("The service was not found.\n");
            }
        else
            {
            printf("Query failed with error %d\n",WSAGetLastError());
            }
        WSACleanup();
        return -1;
        }

    //Call LookupNext to collect an instance to use. Note that this
    //can be called in a loop until WSA_E_NO_MORE is returned to get
    //all the instances returned by the query. Try each instance
    //until one works.

    printf("Client: search successful. Trying to connect...\n");
    dwResultSize = BUFSIZE;
    while (WSALookupServiceNext(hQ,
                                0,
                                &dwResultSize,
                                ResultSet) == NO_ERROR)
    {
    for (iAddr=0;iAddr<ResultSet->dwNumberOfCsAddrs;iAddr++)
        {
        if (connect(s,
            ResultSet->lpcsaBuffer[iAddr].RemoteAddr.lpSockaddr,
            sizeof(SOCKADDR)) == NO_ERROR)
            {
            bConnected = TRUE;
            break;
            }
         else
            {
            printf("Connection failed %d. Error was %d\n",iAddr,WSAGetLastError());
            }
        }
        if (bConnected)
            break;
    }

    //Close out the search service found and connected to

    status = WSALookupServiceEnd(hQ);

    printf("Client: connected!\n");

    //Send the server a message

    printf("Client: Sending message to server...\n");
    status = send(s,(char *)wszClientMessage,sizeof(wszClientMessage),0);

    //Clean up

    WSACleanup();
    printf("Client: Finis!\n");
    return 0;

}


Publishing with Service Connection Points

Active Directory defines an object class, serviceConnectionPoint (SCP) to make it easy for you to "plug in" your service binding and additional information. A setup program (or install option for the service executable) installs the service (by calling CreateService), creates a service account, if necessary, and creates the service connection point. At service startup, the service itself checks to ensure that the SCP contains the correct binding information.

Note Multiple instances of a service may exist in a network. Each instance may have different capabilities; for example, different database servers may contain entirely different data, although they are all the same "kind" of service (usually called "service class"). In addition, services may be replicated. A replicated service is one in which multiple instances exist, each with identical capabilities. With a replicated service, a client can connect to any copy and receive identical service. Active Directory is an example of a replicated service: all domain controllers for a given domain hold identical data and provide identical services. When creating an SCP, you need to consider how clients will locate the SCP for your service. If multiple instances of the service exist, you must consider how the clients will distinguish the service with the desired capabilities from similar services with different capabilities. If you are publishing a replicated service, you must consider how a client will choose a replica from among those offered.


Service-Connection-Point Object

The service-connection-point (SCP) class is a base object class for use in publishing and locating services. SCP is intended for use in publishing client bindings. The properties defined for this class are sufficient for a service to publish the information necessary for a client to bind to an instance of the service. The clients of the service must have previous knowledge of how to interpret and use the binding attribute. Active Directory does not define usage. Services that need to publish additional information about themselves can extend the schema by subclassing the SCP, giving the subclass a distinct name that makes it easily recognizable. Schema extensions are discussed in the Extending the Schema.


Service Connection Point Properties

The serviceConnectionPoint object has the following important properties:

keywords
A property with multiple values that contains string values that help client applications find service connection point objects for a specific type of service. This property is indexed and is replicated to the global catalog. The service publishing the service connection point should add each keyword as an individual element in a property with multiple values.

The following keywords should be included:

  • Vendor GUID. Specify a GUID that represents your company or organization so that a client application can find service connection points for services provided by the company or organization. The GUID should be the string form returned from the UuidToString function in the RPC library. Because GUIDs are ensured of being unique, this value should be used to search for SCPs for a specific vendor.

    For example:

    7A81F122-6A92-11d2-9065-0080C79A9BA5

  • Product GUID. Specify a GUID that represents the product or service so that a client application can find service connection points for services of that type. Since GUIDs are ensured of being unique, this value should be used to search for the SCPs for a specific type of service.

    For example:

    B309E606-6BCA-11d2-906B-005345000000

  • Product Version GUID. Specify a GUID that represents the specific version of the product or service so that a client application can find service connection points for services of that version. Because GUIDs are ensured of being unique, this value should be used to search for the SCPs for a specific versions of a product.

    For example:

    04e3d120-8ef0-11d2-8d4f-00c04f79ab81

  • Vendor Name. Specify the name of the company or organization so that a client application can find service connection points for services provided by the company or organization. This value can be used to search by human-readable name for SCPs for a specific vender.

    For example:

    Microsoft Corporation

  • Product Name. Specify a name that represents the product or service and its version number so that a client application can find service connection points for services of that type. This value can be used to search by human-readable name for SCPs for a specific product and/or version.

    For example:

    Visual C++

  • Product Version. Specify a name that represents the version of the product or service so that a client application can find service connection points for services of that version. This value can be used to search by human-readable name for SCPs for a specific version of a product.

    For example:

    6.0

serviceClassName
A single-valued property that identifies the "class of service" represented by the SCP. This is a descriptive string specific to the service that published the SCP, for example "SqlServer". For services that use mutual authentication, this property will be used to form a Service Principal Name.
serviceBindingInformation
A multi-valued property that contains string values that store information needed to bind to a service. This property is indexed and is replicated to the global catalog.

The content of serviceBindingInformation is specific to the service that published the SCP; clients must know how to interpret the binding information. In the simplest and most common case, the binding information consists of a port number on the server named by serviceDNSName (see the following) at which the service can be contacted.

serviceDNSName
A single-valued property that contains the DNS name of the service. This is typically the DNS host name of the server where the service is running, for example Server.Microsoft.Com. For services that are represented by SRV records in DNS, this property can contain the name of the SRV record, for example _ldap._tcp.microsoft.com.
serviceDNSNameType (Service-DNS-Name-Type)
A single-valued property that specifies the type of DNS name specified by serviceDNSName. The value of this property, if needed, is usually "A" if serviceDNSName contains a host name or "SRV" if serviceDNSName contains a SRV record name.


Creating and Maintaining a Service Connection Point

The guiding principle for creating a service connection point is to avoid publishing stale binding to clients by having the service update binding information at startup.

At startup time, a service must locate any service-related objects it has published and update the binding information, if necessary. In this way, interested clients are assured the most up-to-date information.

Every object stored in Active Directory is assigned an identity when it is created. The identity is a GUID, and is stored in the objectGUID property. The objectGUID never changes, regardless of whether the object is moved or renamed. The objectGUID provides a reliable identity that can always be used to retrieve the object.

When a service creates an SCP, the service must read the objectGUID off the newly-created object and cache it in the Registry of the machine where the service is running. Services running as a specific service account should cache the objectGUID as the default value for:

KKEY_CURRENT_USER
                SOFTWARE
                    <VENDOR-NAME>
                        <PRODUCT-NAME>
                                ObjectGUID

Where <VENDOR-NAME> and <PRODUCT-NAME> identify the vendor and product. Services running as LocalSystem should use the same hierarchy under HKEY_LOCAL_MACHINE. Note that writing values to the HKLM hierarchy requires administrative privileges and is, in general, discouraged.

On each subsequent startup, the service will bind to the SCP referenced by the cached GUID and update the keywords, serviceBindingInformation, serviceDNSName, and serviceDNSNameType attributes if they are incorrect. Services must not unconditionally update their SCPs, as this will cause unnecessary replication.

The directory is optimized for read access. Writing new objects and updating existing objects generates replication traffic. Services should restrict themselves to creating new objects at service installation time and update those objects only when absolutely necessary. (Note: an update request that does not actually change anything in the directory — that is, the the old value — does not generate replication traffic.)


Example Code for Publishing a Service Connection Point

The following function creates a serviceConnectionPoint object that publishes a service:

//ScpCreate

//Create a new Service Connection Point as a child of the local
//server's Computer object

DWORD
ScpCreate(USHORT usPort, TCHAR *pszDn, TCHAR *pszClass, HKEY hkey)
{
    DWORD               dwStat,
                        dwAttr,
                        dwLen;
    int i;

    HRESULT             hr;
    IDispatch           *pDisp; //Returned dispinterface of new object
    IDirectoryObject    *pComp; //Computer object
    IDirectoryObject    *pScp;  //IADs interface on new object

    //Values for setting attributes of SCP

    TCHAR              *KwVal[]=
    {
        TEXT("937924B8-AA44-11d2-81F1-00C04FB9624E"), //Vendor GUID
        TEXT("A762885A-AA44-11d2-81F1-00C04FB9624E"), //Product GUID
        TEXT("Microsoft"),                            //Vendor Name
        TEXT("Windows 2000 Auth-O-Matic"),            //Product Name
    };

    TCHAR       szServer[MAX_PATH];
    TCHAR       szDn[MAX_PATH];
    TCHAR       szAdsPath[MAX_PATH];
    TCHAR       szServiceClass[]=TEXT("SockAuth2");
    TCHAR       szPort[6];
    TCHAR       *szAttrib[]=
    {
        {TEXT("objectGUID")},
        {TEXT("distinguishedName")},
    };

    TCHAR       szGUID[40];

    HKEY        hReg;
    DWORD       dwDisp;

    ADSVALUE    cn,objclass,keywords[4],binding,classname,dnsname,nametype;

    PADS_ATTR_INFO  pAttribs; //To read back new object GUID and DN

    ADS_ATTR_INFO   ScpAttribs[] =
    {
        {TEXT("cn"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&cn,1},
        {TEXT("objectClass"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&objclass,1},
        {TEXT("keywords"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,keywords,4},
        {TEXT("serviceDnsName"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&dnsname,1},
        {TEXT("serviceDnsNameType"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&nametype,1},
        {TEXT("serviceClassName"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&classname,1},
        {TEXT("serviceBindingInformation"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&binding,1},
    };

    //Obtain the DNS name of the local server
    //

    dwLen = sizeof(szServer);
    if (!GetComputerNameEx(ComputerNameDnsFullyQualified,szServer,&dwLen))
        return GetLastError();

    //Fill in the values for the attributes. Use these to create the SCP.
    //

    keywords[0].dwType = ADSTYPE_CASE_IGNORE_STRING;
    keywords[1].dwType = ADSTYPE_CASE_IGNORE_STRING;
    keywords[2].dwType = ADSTYPE_CASE_IGNORE_STRING;
    keywords[3].dwType = ADSTYPE_CASE_IGNORE_STRING;

    keywords[0].CaseIgnoreString=KwVal[0];
    keywords[1].CaseIgnoreString=KwVal[1];
    keywords[2].CaseIgnoreString=KwVal[2];
    keywords[3].CaseIgnoreString=KwVal[3];

    cn.dwType                   = ADSTYPE_CASE_IGNORE_STRING;
    cn.CaseIgnoreString         = TEXT("SockAuth2");
    objclass.dwType             = ADSTYPE_CASE_IGNORE_STRING;
    objclass.CaseIgnoreString   = TEXT("serviceConnectionPoint");

    dnsname.dwType              = ADSTYPE_CASE_IGNORE_STRING;
    dnsname.CaseIgnoreString    = szServer;
    classname.dwType            = ADSTYPE_CASE_IGNORE_STRING;
    classname.CaseIgnoreString  = szServiceClass;

    _stprintf(szPort,TEXT("%d"),usPort);
    binding.dwType              = ADSTYPE_CASE_IGNORE_STRING;
    binding.CaseIgnoreString    = szPort;
    nametype.dwType             = ADSTYPE_CASE_IGNORE_STRING;
    nametype.CaseIgnoreString   = TEXT("A");

    //Obtain the DN of the computer object for this server.

    dwLen = sizeof(szDn);
    if (!GetComputerObjectName(NameFullyQualifiedDN,szDn,&dwLen))
        return GetLastError();

    //Compose the ADSpath and bind to the computer object for this server.

    _tcscpy(szAdsPath,TEXT("LDAP://"));
    _tcscat(szAdsPath,szDn);

    hr = ADsGetObject(szAdsPath,IID_IDirectoryObject,(void **)&pComp);
    if (FAILED(hr))
    {
        ReportError(TEXT("Failed to bind Computer Object."),hr);
        return hr;
    }

    //*******************************************************************

    //Publish the SCP as a child of the computer object.

    //*******************************************************************

    //Figure out attribute count

    dwAttr = sizeof(ScpAttribs)/sizeof(ADS_ATTR_INFO);

    hr = pComp->CreateDSObject(TEXT("cn=SockAuth2"),ScpAttribs,dwAttr,&pDisp);
    if (FAILED(hr))
    {
        ReportError(TEXT("Failed to create SCP:"),hr);
        return hr;
    }

    hr = pDisp->QueryInterface(IID_IDirectoryObject,(void **)&pScp);
    if (FAILED(hr))
    {
        ReportError(TEXT("Failed to QI for IDirectoryObject:"),hr);
        pDisp->Release();
        return hr;
    }

    pDisp->Release();

    hr = pScp->GetObjectAttributes(szAttrib,2,&pAttribs,&dwAttr);
    if (FAILED(hr))
    {
        ReportError(TEXT("Failed to read SCP GUID and DN."),hr);
        pScp->Release();
        return hr;
    }

    for (i=0;i<(LONG)dwAttr;i++) {
        if (_tcscmp(TEXT("objectGUID"),pAttribs[i].pszAttrName)==0) {
        //Convert the returned binary GUID to a string for persisting in the registry;
        //This allows us to quickly retrieve and update the object this instance of our service
        //created on subsequent startups, regardless of renames or moves.

            BuildGUIDString(szGUID,pAttribs[i].pADsValues->OctetString.lpValue);
        }

        if (_tcscmp(TEXT("distinguishedName"),pAttribs[i].pszAttrName)==0)
            _tcscpy(pszDn,pAttribs[i].pADsValues->CaseIgnoreString);
    }
    //Return the service class name.

    _tcscpy(pszClass,szServiceClass);

    //Free the returned attribute structure and ADSI COM interface.

    FreeADsMem(pAttribs);
    pScp->Release();

    //Save the object GUID under <hkey>\SOFTWARE\Vendor\Product.

    dwStat = RegCreateKeyEx(hkey,
                        TEXT("Software\\Microsoft\\Windows 2000 Auth-O-Matic\\ObjectGUID"),
                        0,
                        NULL,
                        REG_OPTION_NON_VOLATILE,
                        KEY_ALL_ACCESS,
                        NULL,
                        &hReg,
                        &dwDisp);
    /*
    ** Cache the new value
    */
    dwStat = RegSetValueEx(hReg,NULL,0,REG_SZ,(const BYTE *)szGUID,2*(_tcslen(szGUID)));

    RegCloseKey(hReg);

    return dwStat;
}


Example Code for Updating the Binding Information in a Service Connection Point

The following function updates a serviceConnectionPoint object for the published service:

//ScpUpdate
//
//Update the SCP written by the local instance of the service. When a service creates an SCP
//it caches the objectGUID of the scp in the registry. At service startup we retrieve the
//saved GUID and attempt to fetch the SCP. If this succeeds, update the SCP with the
//current binding information, if it is different than the stored information.
//
//If the registry key does not exist or the SCP is not found, try to create one by calling
//ScpCreate.

DWORD
ScpUpdate(USHORT usPort, TCHAR *pszDn, TCHAR *pszClass, HKEY hkey)
{
    DWORD   dwStat,
            dwType,
            dwLen;

    bool    bUpdate=false;

    HKEY    hReg;

    TCHAR   szGUID[40];
    TCHAR   szAdsPath[MAX_PATH];
    TCHAR   szServer[MAX_PATH];
    TCHAR   szPort[8];
    TCHAR   *pszAttrs[]=
    {
        {TEXT("serviceDNSName")},
        {TEXT("serviceBindingInformation")},
        {TEXT("serviceClassName")},
        {TEXT("distinguishedName")},
    };

    HRESULT             hr;
    IDirectoryObject    *pObj;
    DWORD               dwAttrs;
    int                 i;

    PADS_ATTR_INFO  pAttribs;
    ADSVALUE        dnsname,binding;

    ADS_ATTR_INFO   Attribs[]=
    {
        {TEXT("serviceDnsName"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&dnsname,1},
        {TEXT("serviceBindingInformation"),ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&binding,1},
    };

    dwStat = RegOpenKeyEx(hkey,
                          TEXT("Software\\Microsoft\\Windows 2000 Auth-O-Matic\\ObjectGUID"),
                          0,
                          KEY_ALL_ACCESS,
                          &hReg);
    if (dwStat == NO_ERROR)
    {
        dwLen = sizeof(szGUID);
        dwStat = RegQueryValueEx(hReg,NULL,0,&dwType,(LPBYTE)szGUID,&dwLen);
        if (dwStat == NO_ERROR)
        {
            _tcscpy(szAdsPath,TEXT("LDAP://<GUID="));
            _tcscat(szAdsPath,szGUID);
            _tcscat(szAdsPath,TEXT(">"));
            hr = ADsGetObject(szAdsPath,IID_IDirectoryObject,(void **)&pObj);
            if (SUCCEEDED(hr))
            {
                dwLen = sizeof(szServer);
                if (!GetComputerNameEx(ComputerNameDnsFullyQualified,szServer,&dwLen))
                {
                    pObj->Release();
                    return GetLastError();
                }
                dnsname.dwType           = ADSTYPE_CASE_IGNORE_STRING;
                dnsname.CaseIgnoreString = szServer;
                _stprintf(szPort,TEXT("%d"),usPort);
                binding.dwType           = ADSTYPE_CASE_IGNORE_STRING;
                binding.CaseIgnoreString = szPort;

                //Read the DNS name and port to see if it needs updating. If they are
                //the same as the current values, there is no need to write them to the directory service. This
                //prevents replicating the SCP if nothing has changed. The attributes
                //asked for can come back in an arbitrary order, so loop through the returned set
                //and check the value based on the attribute that is found.
                //
                hr = pObj->GetObjectAttributes(pszAttrs,4,&pAttribs,&dwAttrs);
                for (i=0;i<(LONG)dwAttrs;i++)
                {
                if (_tcscmp(TEXT("serviceDNSName"),pAttribs[i].pszAttrName)==0)
                    if (_tcscmp(szServer,pAttribs[i].pADsValues->CaseIgnoreString) != 0)
                        bUpdate = true;
                if (_tcscmp(TEXT("serviceBindingInformation"),pAttribs[i].pszAttrName)==0)
                    if (_tcscmp(szPort,pAttribs[i].pADsValues->CaseIgnoreString) != 0)
                        bUpdate = true;

                //Return the DN and Class. These are needed to compose the SPN.
                //Note that best practice is to either accept and return the buffer
                //size or do this in a _try / _except block, both omitted here
                //for the sake of clarity.

                if (_tcscmp(TEXT("serviceClassName"),pAttribs[i].pszAttrName)==0)
                    _tcscpy(pszClass,pAttribs[i].pADsValues->CaseIgnoreString);
                if (_tcscmp(TEXT("distinguishedName"),pAttribs[i].pszAttrName)==0)
                    _tcscpy(pszDn,pAttribs[i].pADsValues->CaseIgnoreString);

                }
                FreeADsMem(pAttribs);

                //The binding information or server name have changed.
                //So, update the SCP with the new values.
                //
                if (bUpdate) {
                    hr = pObj->SetObjectAttributes(Attribs,2,&dwAttrs);
                    if (FAILED(hr))
                    {
                        ReportError(TEXT("WARNING! Update SCP Failed."),hr);
                        pObj->Release();
                        return hr;
                    }
                }
            }
        else
            {  //Bound to object
                    ReportError(TEXT("Failed to bind to SCP."),hr);
                    dwStat = hr;
            } //Bind failed
        } //Read GUID
    } //Open key OK
    RegCloseKey(hReg);

    if (dwStat != NO_ERROR)
        dwStat = ScpCreate(usPort, pszDn, pszClass, hkey);

    return dwStat;
}


Example Code for Querying the Directory for a Service Connection Point

The following function queries for the serviceConnectionPoint object for the published service:

//ScpLocate:
//
//Called by the client to locate the SCPs for this service, within
//the local forest. This is a deep search of the global catalog,
//because it isn't known where an administrator might have moved the
//SCP(s) of interest.

DWORD
ScpLocate(TCHAR *pszDn,DWORD *dwDn, TCHAR *pszServer,DWORD *dwSrv,
          TCHAR *pszClass, DWORD *dwClass, USHORT *pusPort)
{
//For enumerating children of the GC namespace.

HRESULT hr;
IEnumVARIANT *pEnum;
IADsContainer *pCont;
VARIANT var;
IDispatch *pDisp;
BSTR  bstrPath;
ULONG lFetch;
IADs *pADs;
int iRows=0;

//Params for IDirectoryObject.

LPOLESTR szSCPPath = new OLECHAR[MAX_PATH];
IDirectoryObject *pSCP;
ADS_ATTR_INFO       *pPropEntries;
DWORD dwNumAttrGot;

//Structures and Params for IDirectorySearch.

IDirectorySearch    *pSearch;
DWORD               dwPref, dwAttrs;
TCHAR               szQuery[255];
ADS_SEARCH_COLUMN   Col;
ADS_SEARCH_HANDLE   hSearch;
ADS_SEARCHPREF_INFO SearchPref[2];
TCHAR   *szAttribs[]=
    {
        {TEXT("distinguishedName")},
        {TEXT("serviceClassName")},
        {TEXT("serviceDNSName")},
        {TEXT("serviceDNSNameType")},
        {TEXT("serviceBindingInformation")}
    };

//In this sample, most error checking is skipped for brevity's sake.

//First, bind to the GC: namespace container object. The "real" GC DN
//is a single immediate child of the GC: namespace, which must
//be obtained using enumeration.

hr = ADsGetObject(TEXT("GC:"),
                  IID_IADsContainer,
                  (void**) &pCont );

//Fetch an enumeration interface for the GC container.

hr = ADsBuildEnumerator(pCont,&pEnum);

//Now enumerate. There's only one child of the GC: object.

hr = ADsEnumerateNext(pEnum,1,&var,&lFetch);
if (( hr == S_OK ) && ( lFetch == 1 ) )
    {
    pDisp = V_DISPATCH(&var);
    hr = pDisp->QueryInterface( IID_IADs, (void**)&pADs);
    hr = pADs->get_ADsPath(&bstrPath);
    pADs->Release();
    }

//Free the enumerator and root container.

ADsFreeEnumerator(pEnum);
pCont->Release();

//Bail out on failure.

if FAILED(hr)
    {
    ReportError(TEXT("Enumeration failed"),hr);
    return -1;
    }

//At this point bstrPath contains the ADsPath for the current GC.
//Now bind the GC to get the search interface.

hr = ADsGetObject(bstrPath,
                  IID_IDirectorySearch,
                  (void**)&pSearch);
if (FAILED(hr))
    {
    ReportError(TEXT("Failed to bind search root."), hr);
    goto Cleanup;
    }

//Set up a deep search.
//Note that thousands of objects are not expected
//in this example, so ask for 1000 rows/page.

dwPref=sizeof(SearchPref)/sizeof(ADS_SEARCHPREF_INFO);
SearchPref[0].dwSearchPref =    ADS_SEARCHPREF_SEARCH_SCOPE;
SearchPref[0].vValue.dwType =   ADSTYPE_INTEGER;
SearchPref[0].vValue.Integer =  ADS_SCOPE_SUBTREE;

SearchPref[1].dwSearchPref =    ADS_SEARCHPREF_PAGESIZE;
SearchPref[1].vValue.dwType =   ADSTYPE_INTEGER;
SearchPref[1].vValue.Integer =  1000;

hr = pSearch->SetSearchPreference(SearchPref,dwPref);
if (FAILED(hr))
    {
    ReportError(TEXT("Failed to set search prefs."), hr);
    goto Cleanup;
    }

//Search for an exact match on our product GUID.

_tcscpy(szQuery,
         TEXT("keywords=A762885A-AA44-11d2-81F1-00C04FB9624E"));

//Execute the search. From the GC, get the distinguished name
//of the SCP. Use the DN to bind to the SCP and get the other
//properties.
//Only expect 1 row in the result set,
//but this example shows various results.

hr = pSearch->ExecuteSearch(szQuery,
                            szAttribs,
                            1,
                            &hSearch);
if (FAILED(hr))
    {
    ReportError(TEXT("ExecuteSearch failed."), hr);
    goto Cleanup;
    }

hr = pSearch->GetFirstRow(hSearch);
while (SUCCEEDED(hr) && hr !=S_ADS_NOMORE_ROWS)
    {
    hr = pSearch->GetColumn(hSearch,TEXT("distinguishedName"),&Col);
    if (FAILED(hr)) break;
    _tprintf(TEXT("DN: %s\n"), Col.pADsValues->CaseIgnoreString);
    _tcscpy(pszDn, Col.pADsValues->CaseIgnoreString);
    pSearch->FreeColumn(&Col);
    hr = pSearch->GetNextRow(hSearch);
    } //No more rows
if FAILED(hr)
    {
    ReportError(TEXT("Failed to process row."),hr);
    goto Cleanup;
    }

//Now bind to the DN to get the other properties.

wcscpy(szSCPPath,L"LDAP://");
wcscat(szSCPPath, pszDn);
hr = ADsGetObject(szSCPPath,
                  IID_IDirectoryObject,
                  (void**)&pSCP);
if (SUCCEEDED(hr)) {
    dwAttrs=sizeof(szAttribs)/sizeof(LPWSTR);
    hr = pSCP->GetObjectAttributes( szAttribs,
                                     dwAttrs,
                                     &pPropEntries,
                                     &dwNumAttrGot);
    if(FAILED(hr))
    {
        ReportError(TEXT("GetObjectAttributes Failed."), hr);
        goto Cleanup;
    }

    //Loop through the entries returned by GetObjectAttributes
    //and save the values in the appropriate variables.

    for (int i=0;i<(LONG)dwAttrs;i++)
    {
        if (_tcscmp(TEXT("distinguishedName"),
                    pPropEntries[i].pszAttrName) ==0)
        {
            _tprintf(TEXT("DN: %s\n"),
                    pPropEntries[i].pADsValues->CaseIgnoreString);
            _tcscpy(pszDn,
                    pPropEntries[i].pADsValues->CaseIgnoreString);
        }

        if (_tcscmp(TEXT("serviceDNSName"),
                    pPropEntries[i].pszAttrName)==0)
        {
            _tprintf(TEXT("Server: %s\n"),
                    pPropEntries[i].pADsValues->CaseIgnoreString);
            _tcscpy(pszServer,
                     pPropEntries[i].pADsValues->CaseIgnoreString);
        }

        if (_tcscmp(TEXT("serviceClassName"),
                    pPropEntries[i].pszAttrName)==0)
        {
            _tprintf(TEXT("Class: %s\n"),
                    pPropEntries[i].pADsValues->CaseIgnoreString);
            _tcscpy(pszClass,
                    pPropEntries[i].pADsValues->CaseIgnoreString);
        }

        if (_tcscmp(TEXT("serviceBindingInformation"),
                    pPropEntries[i].pszAttrName)==0)
        {
            _tprintf(TEXT("Port: %s\n"),
                    pPropEntries[i].pADsValues->CaseIgnoreString);
            *pusPort=(USHORT)_ttoi(
                    pPropEntries[i].pADsValues->CaseIgnoreString);
        }
    }
}

Cleanup:
pSCP->Release();
FreeADsMem(pPropEntries);
pSearch->CloseSearchHandle(hSearch);
return hr;
}


Publishing COM+ Services

COM-based services provide an "application proxy" in the form of an MSI installation package. This MSI file contains the server name to be used and other "glue" like proxy/stubs and type libraries needed for marshalling. The Component Services snap-in automatically generates these application proxies for COM+ Server applications.

The "Application Proxies" are published into policy objects in Active Directory using the Group Policy Editor. No special intervention is required in the client application. The machine/user account on the client machine needs to be in an OU that is configured to use the policy object in which the application proxies are published. The COM Binder will automatically locate the server using the Directory when the client instantiates the object in question.


Mutual Authentication Using Kerberos

Mutual authentication is a security feature in which a client process must prove its identity to a service, and the service must prove its identity to the client, before any application traffic is sent over the client/service connection. The ability of a client to authenticate the service to which it is connecting is particularly important in client/service applications that support delegation of the client's security context.

Active Directory™ and Windows® 2000 provide support for service principal names (SPNs), which are a key component in mutual authentication. An SPN is a unique name that identifies an instance of a service. A service (or an administrator on behalf of the service) registers its SPN or SPNs in the directory when the service is installed. A client composes an SPN for its service, using known information or information retrieved from a trusted source such as the directory. The client then uses the SPN to authenticate the service.

This chapter discusses the following topics:

  • An overview of mutual authentication using Kerberos.
  • How to compose and register service principal names.
  • How a Windows Sockets client/service application can perform mutual authentication using service connection points (SCPs) and the Security Support Provider Interface (SSPI).
  • How an RPC client/service application can perform mutual authentication using the RPC name service and RPC authentication.
  • How a Windows Sockets RnR service uses SPNs to perform mutual authentication.

This chapter focuses on using Active Directory for mutual authentication, in particular, the role that service connection points and service principal names play in mutual authentication. It is not intended to exhaustively document how to use SSPI for mutual authentication or the authentication and security support available for RPC and Windows Sockets applications.

For more information, see the following topics:

  • Publishing with service connection points
  • SSPI
  • Windows Sockets
  • RPC


About Mutual Authentication Using Kerberos

Mutual authentication is conceptually quite simple. The client and service must prove their respective identities to each other before performing any application functions. The central principal of mutual authentication is that neither party must "trust" the other before identity has been proven. What this means in practical terms is that the service must be able to determine who the client is without asking the client and the client must be able to determine who the service is without asking the service.

The service authenticates the client as follows. The client establishes a local security context either by executing in a previously established context (for example, in the session of a logged-in user) or by explicitly presenting credentials to the underlying security provider. The service will not accept connections from any client that is not authenticated.

The client authenticates the service by composing a service principal name (SPN) based on information it already knows about the service or which it obtains from another trusted source, such as the directory. The service itself is not a trusted source until it is authenticated. The client presents the SPN to the security system, so the service can authenticate itself using the presented SPN. The client will not communicate further with a service that cannot authenticate the SPN.

A service composes its SPN (or SPNs in the case of a replicated service) using elements that make the names unique. For a discussion of SPN name composition and the functions for working with SPNs, see Service Principal Names. For sample code that composes an SPN for a Windows Sockets service that publishes itself with a service connection point (SCP), see Composing the SPNs for a Service with an SCP. For sample code that composes an SPN for an RPC service, see Composing SPNs for an RpcNs Service.

A service calls the DsWriteAccountSpn function to register one or more SPNs under a user or computer account object in the directory. Objects of class user or computer have a servicePrincipalName attribute, which is a multi-valued attribute for storing all the SPNs associated with a user or computer account. If the service runs under a service account, the SPNs are stored in the servicePrincipalName attribute of that account. If the service runs in the LocalSystem account, the SPNs are stored in the servicePrincipalName attribute of the account of the local computer. The DsWriteAccountSpn caller must specify the distinguished name of the account object under which the SPNs are stored.

To ensure that registered SPNs are secure, the servicePrincipalName attribute cannot be written directly; it can only be written by calling DsWriteAccountSpn. The DsWriteAccountSpn caller must have write access to the servicePrincipalName attribute of the target account. For a user account, this write access is granted by default only to domain administrators. For a computer account, this write access is also granted to the computer account itself if the SPN being written is in the form serviceclass/host. This enables a service running under the LocalSystem account to call DsWriteAccountSpn to register SPNs on the computer account of the service's host. For sample code that registers a service's SPNs, see Registering the SPNs for a Service.

Security Providers

Support for mutual authentication is provided by the Security Support Provider Interface (SSPI) and is exposed directly through the SSPI APIs and services that layer upon SSPI, including RPC.

Not all security packages available to SSPI support mutual authentication. To obtain mutual authentication, the application must request mutual authentication and a security package that supports it. For example, the sample code in Mutual Authentication in a Windows Sockets Service with an SCP uses the "negotiate" package in Secur32.dll, which ships with Windows 2000.

Integrity and Privacy

Client/service communications that require mutual authentication must also take care to protect the traffic they exchange after successful authentication. It does no good to mutually authenticate at the time of the initial connection to the service if the traffic is later subject to malicious modification by an attacker. SSPI, RPC, and COM all provide facilities for digitally signing and encrypting messages. Applying digital signatures prevents undetected modification of traffic; encryption discourages eavesdropping. Interception of traffic is possible, of course, but decrypting the traffic can be made sufficiently difficult to deter most attackers.

Unless performance requirements are very stringent, use of both signing and encryption is highly recommended, especially for administrative type functions. It's better to be safe than sorry. Even when performance is an issue, some customers may opt for tighter security over better performance. In such cases, make integrity and privacy configurable options.

RPC clients can specify the level of integrity and privacy when they call the RpcBindingSetAuthInfoEx function to set the authentication information for the RPC binding. Use RPC_C_AUTHN_LEVEL_PKT_INTEGRITY for signing and RPC_C_AUTHN_LEVEL_PKT_PRIVACY for encryption. For sample code that shows the use of these flags, see Client Authentication of an RpcNs Service.

Services that use an SSPI package for mutual authentication can query the package to determine whether it supports the MakeSignature, VerifySignature, EncryptMessage, and DecryptMessage functions for signing and encrypting messages. For sample code that illustrates the use of these functions, see Ensuring Communication Integrity During Message Exchange in the SSPI documentation.

Limitations of Mutual Authentication with Kerberos

If the service's account is in a different forest than the client's account, mutual authentication will fail because Kerberos will not be able to find the service account.

Both service and client must be running on Windows 2000 computers; otherwise, mutual authentication with Kerberos will fail because earlier versions of Windows does not support Kerberos.

Service principal names include the DNS name of the host server on which the service is running. You must use the DNS name; NetBIOS names are not supported.


Service Principal Names

Service principal names (SPNs) are associated with the security principal (User or Computer) in whose security context the service executes. SPNs are used to support mutual authentication between a client application and a service. An SPN is assembled from information a client knows about a service or can obtain from a trusted third party, such as the directory.

To use SPNs, a service and client perform the following general steps. Specific steps to use with Windows Sockets and RPC applications are presented in the next sections.

To register SPNs in the directory at service installation

  1. Create a set of SPN's for the service with the DsGetSpn function.
  2. Register the SPN's on the security principal for the service with the DsWriteAccountSpn function. This step must be performed by a domain administrator (except that a service running under the LocalSystem account can register its SPN in the form serviceclass/host on the computer account of the service's host).

At service startup

No action required.

To authenticate the service at client startup

  1. Compose an SPN for the service with the DsMakeSpn function.
  2. Present the SPN when requesting mutual authentication (details vary depending on binding mechanism).

A service uses the following elements to compose an SPN:

service class name
This is a string identifying the general class of service, for example, "SqlServer". This can be any string that is unique to the service.
service name
A name identifying the specific service. For simple services that run under the LocalSystem account on a single host server, this can be the DNS host name of the server. For services published in Active Directory, this is the distinguished name (DN) of the service's connection point.
service instance name
The DNS host name of the server where the service is executing. Services that have multiple instances on a server can use multiple instance names and ports to compose unique names for each instance.
service instance port
If multiple instances of the same service can run on a single server (for example, WWW service) this is the port number for contacting a specific instance of the service.

SPNs are required to be unique within a forest. The components of an SPN are designed to produce unique SPNs. Note that DsWriteAccountSpn does not check to ensure that the SPN being written is unique. However, the servicePrincipalName attribute is included in the global catalog, so a service can search the GC to ensure that its SPN is unique. For information about searching the GC, see Searching Global Catalog Contents.

Windows 2000 provides a set of Win32 APIs to compose and register service principal names:

DsGetSpn
Called by the service or a party acting on behalf of the service to create service principal names to register for that service. The DsGetSpn function takes as input the service class, service name, service port (if any), and additional names and ports (if any), and returns an array of SPNs composed of the provided information and the DNS Name of the local computer.
DsMakeSpn
Called by a client to compose an SPN for a service for use in mutual authentication. The DsMakeSpn function takes as input the service class, the distinguished name of the service's connection point, the DNS name of the service's host, and service instance port (if applicable). The client presents the resultant SPN when requesting mutual authentication.
DsWriteAccountSpn
Registers the SPNs for a service. The DsWriteAccountSpn function writes one or more SPNs to the servicePrincipalName attribute of a user or computer account object in the directory. The servicePrincipalName attribute is a multi-valued attribute of objects of class user and computer. The parameters to DsWriteAccountSpn include the following:
  • The distinguished name of the account object.
  • The SPNs, which are typically composed using the DsGetSpn function.
  • An operation parameter that indicates whether the SPNs are to be added to the account, removed from the account, or used to completely replace all current SPNs for the account.

A DsWriteAccountSpn "Add" operation does not write the same SPN more than once to a given user or computer account. If a specified SPN already exists on the account, DsWriteAccountSpn simply returns without reporting an error.

DsFreeSpnArray
Frees an array returned from the DsGetSpn function.


Mutual Authentication in a Windows Sockets Service with an SCP

This section provides sample code that shows how to perform mutual authentication with a service that publishes itself using a service connection point (SCP). The example is based on a Windows Sockets service that uses an SSPI package to handle the mutual authentication negotiation between a client and the service.

The example follows these steps to implement mutual authentication:

To register SPNs in the directory at service installation

  1. Call the DsGetSpn function to compose service principal names (SPNs) for the service.
  2. Call the DsWriteAccountSpn function to register the SPNs on the service account or computer account in whose context the service will run. This step must be performed by a domain administrator (except that a service running under the LocalSystem account can register its SPN in the form serviceclass/host on the computer account of the service's host).

At service startup

No action required.

To authenticate the service at client startup

  1. Retrieve connection information from the service's connection point.
  2. Establish a connection to the service.
  3. Call the DsMakeSpn function to compose an SPN for the service. Compose the SPN from the known service class string, and the information retrieved from the service connection point. This information includes the distinguished name of the service connection point and the host name of the server on which the service is running. Note that the host name must be a DNS name; NetBIOS names are not supported.
  4. Use an SSPI security package to perform the authentication.
    1. Call the AcquireCredentialsHandle function to acquire the client's credentials.
    2. Pass the client credentials and the SPN to the InitializeSecurityContext function to generate a security blob to send to the service for authentication. Set the ISC_REQ_MUTUAL_AUTH flag to request mutual authentication.
    3. Exchange blobs with the service until the authentication is complete.
  5. Check the returned capabilities mask for the ISC_RET_MUTUAL_AUTH flag to verify that mutual authentication was performed.
  6. If the authentication was successful, exchange traffic with the authenticated service. Use digital signing to ensure that messages between client and service have not been tampered with. Unless performance requirements are very stringent, you should also use encryption. For sample code that illustrates the use of the MakeSignature, VerifySignature, EncryptMessage, and DecryptMessage functions in an SSPI package, see Ensuring Communication Integrity During Message Exchange in the SSPI documentation.

To authenticate the client by the service when a client connects

  1. Load an SSPI security package that supports mutual authentication.
  2. When a client connects, use the security package to perform the authentication.
    1. Call the AcquireCredentialsHandle function to acquire the service's credentials.
    2. Pass the service credentials and the security blob received from the client to the AcceptSecurityContext function to generate a security blob to send back to the client. Set the ASC_REQ_MUTUAL_AUTH flag to request mutual authentication.
    3. Exchange blobs with the client until the authentication is complete.
  3. Check the returned capabilities mask for the ASC_RET_MUTUAL_AUTH flag to verify that mutual authentication was performed.
  4. If the authentication was successful, exchange traffic with the authenticated client. Use digital signing. Use encryption as well, unless performance is an issue.

The following topics provide sample code for this mutual authentication scenario.

  • Client Authentication with Windows Sockets and SCP
  • Composing and Registering SPNs for a Windows Sockets Service
  • How a Windows Sockets Service Authenticates a Client

For more information, see the following topics:

  • Publishing with service connection points
  • SSPI documentation
  • Windows Sockets documentation


Client Authentication with Windows Sockets and SCP

This topic shows the code that a client program uses to compose an SPN for a service. The client binds to the service's service connection point (SCP) to retrieve the information needed to connect to the service. The SCP also contains information the client can use to compose the service's SPN. For sample code that binds to the SCP and retrieves the necessary properties, see Example Code for Querying the Directory for a Service Connection Point.

This topic also shows how a client uses an SSPI security package and the service's SPN to establish a mutually authenticated connection to the Windows Sockets service. Note that this code is almost identical to the code required in Windows NT 4.0 and earlier just to authenticate the client to the server. The only difference is that the client must supply the SPN and specify the ISC_REQ_MUTUAL_AUTH flag.

Note that in composing the SPN, the client must use the DNS name of the service's server. NetBIOS names are not supported.

Client Code to Make an SPN for a Service

// Initialize these strings by querying the service's SCP.
TCHAR szDn[MAX_PATH],     // DN of the service's SCP
      szServer[MAX_PATH], // DNS name of the service's server
      szClass[MAX_PATH];  // Service class

TCHAR szSpn[MAX_PATH];    // Buffer for SPN
SOCKET sockServer;        // Socket connected to service
DWORD dwRes, dwLen;

// Compose the SPN for the service using the DN, service class, and
// server strings retrieved from the service's SCP.
dwLen = sizeof(szSpn);
dwRes = DsMakeSpn(
    szClass,    // Service class
    szDn,       // DN of the service's SCP
    szServer,   // DNS name of the server on which service is running
    0,          // Default port
    NULL,       // No referrer
    &dwLen,     // Size of szSpn buffer
    szSpn);     // Buffer to receive the SPN

if (!DoAuthentication (szServer, sockServer, szSpn))
{
    closesocket (sockServer);
    return(FALSE);
}
.
.
.

Client Code to Authenticate the Service

This code sample consists of two routines.

The DoAuthentication routine calls the GenClientContext to generate the initial buffer to send to the service. DoAuthentication uses the socket handle to send the buffer and receive the service's response, which is passed to the SSPI package by another call to GenClientContext. This loop is repeated until the authentication fails or GenClientContext sets a flag that indicates the authentication was successful.

The GenClientContext routine interacts with the SSPI package to generate the authentication information to send to the service and process the information received from the service. The key components of the authentication information provided by the client are the following:

  • The service principal name which identifies the credentials that the service must authenticate.
  • The client's credentials. The AcquireCredentialsHandle function of the security package extracts these credentials from the client's security context which was established at logon.
  • To request mutual authentication, the client must specify the ISC_REQ_MUTUAL_AUTH flag when it calls the InitializeSecurityContext function during the GenClientContext routine.
// Structure for storing the state of the authentication sequence.
typedef struct _AUTH_SEQ
{
    BOOL _fNewConversation;
    CredHandle _hcred;
    BOOL _fHaveCredHandle;
    BOOL _fHaveCtxtHandle;
    struct _SecHandle  _hctxt;
} AUTH_SEQ, *PAUTH_SEQ;

/***************************************************************/
//   DoAuthentication routine for the client.
//
//   Manages the client's authentication conversation with the service
//   using the supplied socket handle.
//
//   Returns TRUE if the mutual authentication is successful.
//   Otherwise, it returns FALSE.
//
/***************************************************************/
BOOL DoAuthentication (
        SOCKET s,
        LPTSTR szSpn)
{
BOOL done = FALSE;
DWORD cbOut, cbIn;

// Call the security package to generate the initial buffer of
// authentication information to send to the service.
cbOut = g_cbMaxMessage;
if (!GenClientContext (s, NULL, 0, g_pOutBuf,
                       &cbOut, &done, szSpn))
    return(FALSE);

if (!SendMsg (s, g_pOutBuf, cbOut))
    return(FALSE);

// Pass the service's response back to the security package, and send
// the package's output back to the service. Repeat until done.
while (!done)
{
    if (!ReceiveMsg (s, g_pInBuf, g_cbMaxMessage, &cbIn))
        return(FALSE);

    cbOut = g_cbMaxMessage;
    if (!GenClientContext (s, g_pInBuf, cbIn, g_pOutBuf,
                           &cbOut, &done, szSpn))
        return(FALSE);

    if (!SendMsg (s, g_pOutBuf, cbOut))
        return(FALSE);
}

return(TRUE);
}

/***************************************************************/
//   GenClientContext routine
//
//   Handles the client's interactions with the security package.
//   Optionally takes an input buffer coming from the service
//   and generates a buffer of information to send back to the
//   service. Also returns an indication when the authentication
//   is complete.
//
//   Returns TRUE if the mutual authentication is successful.
//   Otherwise, it returns FALSE.
//
/***************************************************************/
BOOL GenClientContext (
            DWORD dwKey,     // socket handle used as key
            BYTE *pIn,
            DWORD cbIn,
            BYTE *pOut,
            DWORD *pcbOut,
            BOOL *pfDone,
            LPTSTR szSpn)
{
SECURITY_STATUS  ssStatus;
TimeStamp        Lifetime;
SecBufferDesc    OutBuffDesc;
SecBuffer        OutSecBuff;
SecBufferDesc    InBuffDesc;
SecBuffer        InSecBuff;
ULONG            ContextAttributes;
PAUTH_SEQ        pAS; // structure to store state of authentication

// Get structure containing the state of the authentication sequence.
if (!GetEntry (dwKey, (PVOID*) &pAS))
    return(FALSE);

if (pAS->_fNewConversation)
{
    ssStatus = g_pFuncs->AcquireCredentialsHandle (
            NULL,    // principal
            PACKAGE_NAME,
            SECPKG_CRED_OUTBOUND,
            NULL,    // LOGON id
            NULL,    // authentication data
            NULL,    // get key function
            NULL,    // get key argument
            &pAS->_hcred,
            &Lifetime
            );
    if (SEC_SUCCESS (ssStatus))
        pAS->_fHaveCredHandle = TRUE;
    else
    {
        fprintf (stderr,
                 "AcquireCredentialsHandle failed: %u\n", ssStatus);
        return(FALSE);
    }
}

// Prepare output buffer
OutBuffDesc.ulVersion = 0;
OutBuffDesc.cBuffers = 1;
OutBuffDesc.pBuffers = &OutSecBuff;

OutSecBuff.cbBuffer = *pcbOut;
OutSecBuff.BufferType = SECBUFFER_TOKEN;
OutSecBuff.pvBuffer = pOut;

// Prepare input buffer
if (!pAS->_fNewConversation)
{
    InBuffDesc.ulVersion = 0;
    InBuffDesc.cBuffers = 1;
    InBuffDesc.pBuffers = &InSecBuff;

    InSecBuff.cbBuffer = cbIn;
    InSecBuff.BufferType = SECBUFFER_TOKEN;
    InSecBuff.pvBuffer = pIn;
}

_tprintf(TEXT("InitializeSecurityContext: pszTarget=%s\n"),szSpn);

ssStatus = g_pFuncs->InitializeSecurityContext (
                     &pAS->_hcred,
                     pAS->_fNewConversation ? NULL : &pAS->_hctxt,
                     szSpn,
                     ISC_REQ_MUTUAL_AUTH,      // context requirements
                     0,                        // reserved1
                     SECURITY_NATIVE_DREP,
                     pAS->_fNewConversation ? NULL : &InBuffDesc,
                     0,                        // reserved2
                     &pAS->_hctxt,
                     &OutBuffDesc,
                     &ContextAttributes,
                     &Lifetime
                     );
if (!SEC_SUCCESS (ssStatus))
{
    fprintf (stderr, "init context failed: %X\n", ssStatus);
    return FALSE;
}

pAS->_fHaveCtxtHandle = TRUE;

// Complete token -- if applicable
if ( (SEC_I_COMPLETE_NEEDED == ssStatus) ||
     (SEC_I_COMPLETE_AND_CONTINUE == ssStatus))
{
    if (g_pFuncs->CompleteAuthToken)
    {
        ssStatus = g_pFuncs->CompleteAuthToken (&pAS->_hctxt,
                                                &OutBuffDesc);
        if (!SEC_SUCCESS(ssStatus))
        {
            fprintf (stderr, "complete failed: %u\n", ssStatus);
            return FALSE;
        }
    } else
    {
        fprintf (stderr, "Complete not supported.\n");
        return FALSE;
    }
}

*pcbOut = OutSecBuff.cbBuffer;

if (pAS->_fNewConversation)
    pAS->_fNewConversation = FALSE;

*pfDone = !((SEC_I_CONTINUE_NEEDED == ssStatus) ||
            (SEC_I_COMPLETE_AND_CONTINUE == ssStatus));

// Check for the ISC_RET_MUTUAL_AUTH flag to verify that
// mutual authentication was performed.
if (*pfDone && !(ContextAttributes && ISC_RET_MUTUAL_AUTH) )
    _tprintf(TEXT("Mutual Auth not set in returned context.\n"));

return TRUE;
}


Composing and Registering SPNs for a Windows Sockets Service

The following code fragment shows how to compose and register the SPNs for a service. Call this code from your service's installation program after calling CreateService and creating the service's service connection point (SCP).

The following code fragment calls the SpnCompose and SpnRegister routines that compose and register the SPN. For the SpnCompose source code, see Composing the SPNs for a Service with an SCP. For the SpnRegister source code, see Registering the SPNs for a Service.

This example uses the service's class name and the distinguished name of its SCP to create its service principal name. For sample code that shows how the client binds to the service's SCP to retrieve these name strings, see Example Code for Querying the Directory for a Service Connection Point. Note that the code for composing an SPN varies depending on the type of service and the mechanisms used to publish the service.

The service registers its SPN by storing it in the servicePrincipalName attribute of the service's account object in the directory. If the service runs under the LocalSystem account instead of under a service account, it registers its SPN under the local computer account's object in the directory.

TCHAR szDn[MAX_PATH];    // DN of SCP. Initialize by querying SCP.
TCHAR szClass[MAX_PATH]; // Service class. Initialize by querying SCP.
LPCTSTR pszServiceAcct;  // Service account passed to CreateService.

DWORD dwStatus;
TCHAR **pspn = NULL;
ULONG ulSpn = 1;

// Compose the SPNs
dwStatus = SpnCompose(
           &pspn,      // Receives an array of SPNs.
           &ulSpn,     // Receives the number of SPNs in array
           szDn,       // DN of the service's SCP
           szClass);   // Name of the service's class

// Register the SPNs
dwStatus = SpnRegister(
           pszServiceAcct,      // Service account specified to CreateService
           pspn,                // Array of SPNs
           ulSpn,               // Number of SPNs in array
           DS_SPN_ADD_SPN_OP);  // Add SPNs to the account

// Free the array of SPNs returned by SpnCompose.
DsFreeSpnArray(ulSpn, pspn);

You can use similar code to unregister your SPNs when your service is being uninstalled. Simple specify the DS_SPN_DELETE_SPN_OP operation instead of DS_SPN_ADD_SPN_OP.


Composing the SPNs for a Service with an SCP

The following code fragment composes an SPN for a service that uses a service connection point.

DWORD
SpnCompose(
    TCHAR ***pspn,          // Output: an array of SPNs
    unsigned long *pulSpn,  // Output: the number of SPNs returned
    TCHAR *pszDn,           // Input: the name of the service's SCP
    TCHAR* pszClass)        // Input: the name of the service's class
{
DWORD   dwStatus;

dwStatus = DsGetSpn(
    DS_SPN_SERVICE,   // Type of SPN to create (enumerated type)
    pszClass,         // Service class - a name in this case
    pszDn,            // Service name - DN of the service's SCP
    0,                // Instance port: 0 = default,
                      //     1 instance per computer
    0,                // Number of additional instance names - 0
    NULL,             // Additional instance names - none, pass NULL
    NULL,             // Additional instance ports - none, pass NULL
    pulSpn,           // Size of SPN array
    pspn              // Returned SPN(s)
    );

return dwStatus;
}


Registering the SPNs for a Service

The following code fragment registers or unregisters the service principal names (SPNs) under a service's account.

The example calls the DsWriteAccountSpn function, which stores the SPNs in the directory under the servicePrincipalName attribute of the service account specified by the pszServiceAcct parameter.

The account name specified to DsWriteAccountSpn must be the distinguished name (DN) of the account object in the directory. Note that in this sample routine, pszServiceAcct can be either a distinguished name or a name in domain\account format. The sample uses the DsCrackNames function to convert a domain\account name to a distinguished name.

If the service runs in the LocalSystem account, pszServiceAcct is NULL. In this case, the sample retrieves the DN of the local computer object in the directory and stores the SPNs under that account.

// SpnRegister
// Register or unregister the SPNs under the service's account.

DWORD
SpnRegister(
    TCHAR *pszServiceAcct,      // Account that the service runs under
    TCHAR **pspn,               // Array of SPNs
    unsigned long ulSpn,        // Number of SPNs in array
    DS_SPN_WRITE_OP Operation)  // Add, replace, or delete SPNs
{

DWORD dwStatus;
HANDLE hDs;
TCHAR szAccount[MAX_PATH];
PDS_NAME_RESULT pRes;
ULONG dwLen = sizeof(szAccount);
WCHAR szSamName[48];
DWORD dwSize = 48;
WCHAR *pWhack = NULL;
PDOMAIN_CONTROLLER_INFO pDcInfo;

// Get the domain for the current user.
if ( GetUserNameEx( NameSamCompatible, szSamName, &dwSize ) )
{
    pWhack = wcschr( szSamName, L'\\' );
    if ( pWhack )
        *pWhack = L'\0';
} else
{
    _tprintf(TEXT("GetUserNameEx failed - %d\n"), GetLastError());
    return GetLastError() ;
}

// Get the name of a domain controller in that domain.
dwStatus = DsGetDcName(
                    NULL,
                    szSamName,
                    NULL,
                    NULL,
                    DS_IS_FLAT_NAME |
                    DS_RETURN_DNS_NAME |
                    DS_DIRECTORY_SERVICE_REQUIRED,
                    &pDcInfo );
if ( dwStatus != 0 )
{
    _tprintf(TEXT("DsGetDcName failed - %d\n"), dwStatus);
    return dwStatus;
}

// Bind to the domain controller.
dwStatus = DsBind( pDcInfo->DomainControllerName, NULL, &hDs );

// Free the DOMAIN_CONTROLLER_INFO buffer.
NetApiBufferFree( pDcInfo );
if ( dwStatus != 0 )
{
    _tprintf(TEXT("DsBind failed - %d\n"), dwStatus);
    return dwStatus;
}

// Get the distinguished name of the account where SPNs are stored.
// If pszServiceAcct is non-NULL, it specifies the name of the
// account.
if (pszServiceAcct)
{
    // First, try to convert the name in case it was specified in
    // domain\account format. If the conversion fails, assume the name
    // is already in distinguished name format.
    dwStatus = DsCrackNames(
             hDs,                 // handle to the directory
             DS_NAME_NO_FLAGS,
             DS_NT4_ACCOUNT_NAME, // input name format: domain/account
             DS_FQDN_1779_NAME,   // output format: fully qualified DN
             1,                   // number of names to convert
             &pszServiceAcct,     // input name
             &pRes);              // receives results

    // If name conversion was successful, use the returned name.
    // Otherwise, use the input name.
    if (NO_ERROR == dwStatus &&
            DS_NAME_NO_ERROR == pRes->rItems[0].status)
    {
        _tcscpy(szAccount, pRes->rItems[0].pName);
        DsFreeNameResult(pRes);
    } else
    {
        _tcscpy(szAccount, pszServiceAcct);
    }
} else
{
    // If no account is specified, the service runs in the LocalSystem
    // account, so store the SPNs under the local computer account.
    // Get the DN of the local computer account.
    dwStatus = GetComputerObjectName(NameFullyQualifiedDN,
                             szAccount, &dwLen);
}

_tprintf(TEXT("SPN is:%s\n"), pspn[0]);
if (Operation == DS_SPN_ADD_SPN_OP)
    _tprintf(TEXT("SPN will be set for %s\n"), szAccount);
else
    _tprintf(TEXT("SPN will be removed from %s\n"), szAccount);

// Write the SPNs to the service account or computer account.
dwStatus = DsWriteAccountSpn(
        hDs,         // handle to the directory
        Operation,   // Add or remove SPN from account's existing SPNs
        szAccount,   // DN of service account or computer account
        ulSpn,                   // Number of SPNs to add
        (const TCHAR **)pspn);   // Array of SPNs
if (dwStatus != NO_ERROR)
    _tprintf(TEXT("Failed to write SPN: Error was %X\n"),dwStatus);

// Unbind the DS in any case.
DsUnBind(&hDs);

return(dwStatus);
}


How a Windows Sockets Service Authenticates a Client

When a client connects to the Windows Sockets service, the service begins its side of the mutual authentication sequence, which is shown in the following code samples.

The DoAuthentication routine uses the socket handle to receive the first authentication packet from the client. The client buffer is passed to the GenServerContext routine, which, in turn, passes the buffer to the SSPI security package for authentication. DoAuthentication then sends the security package output back to the client. This loop is repeated until the authentication fails or GenServerContext sets a flag indicating the authentication was successful.

GenServerContext calls the following functions from an SSPI security package.

  • AcquireCredentialsHandle, which extracts the service's credentials from the service's security context that was established when the service started.
  • AcceptSecurityContext, which attempts to perform the mutual authentication using the service's credentials and the authentication information received from the client. To request mutual authentication, the AcceptSecurityContext call must specify the ASC_REQ_MUTUAL_AUTH flag.
  • CompleteAuthToken, which is called, if necessary, to complete the authentication operation.

This sample uses the "negotiate" package from the secur32.dll library of security packages.

/***************************************************************/
//   DoAuthentication routine for the service
//
//   Manages the service's authentication conversation with the client
//   using the supplied socket handle.
//
//   Returns TRUE if the mutual authentication is successful.
//   Otherwise, it returns FALSE.
//
/***************************************************************/

BOOL DoAuthentication (SOCKET s)
{
DWORD cbIn, cbOut;
BOOL done = FALSE;

// Receive authentication information from the client and pass
// it to the security package. Send the package's output back
// to the client. Repeat until done.
do
{
    if (!ReceiveMsg (s, g_pInBuf, g_cbMaxMessage, &cbIn))
        return(FALSE);

    cbOut = g_cbMaxMessage;
    if (!GenServerContext (s, g_pInBuf, cbIn, g_pOutBuf,
                               &cbOut, &done))
        return(FALSE);

    if (!SendMsg (s, g_pOutBuf, cbOut))
        return(FALSE);
}
while(!done);

return(TRUE);
}

/***************************************************************/
//   GenServerContext routine
//
//   Handles the service's interactions with the security package.
//   Takes an input buffer coming from the client and generates a
//   buffer of information to send back to the client. Also returns
//   an indication when the authentication is complete.
//
//   Returns TRUE if the mutual authentication is successful.
//   Otherwise, it returns FALSE.
//
/***************************************************************/
BOOL GenServerContext (
            DWORD dwKey,
            BYTE *pIn,
            DWORD cbIn,
            BYTE *pOut,
            DWORD *pcbOut,
            BOOL *pfDone)
{
SECURITY_STATUS  ssStatus;
TimeStamp        Lifetime;
SecBufferDesc    OutBuffDesc;
SecBuffer        OutSecBuff;
SecBufferDesc    InBuffDesc;
SecBuffer        InSecBuff;
ULONG            ContextAttributes;
PAUTH_SEQ        pAS;

// Get structure containing the state of the authentication sequence.
if (!GetEntry (dwKey, (PVOID*) &pAS))
    return(FALSE);

if (pAS->_fNewConversation)
{
    ssStatus = g_pFuncs->AcquireCredentialsHandle (
                        NULL,    // principal
                        PACKAGE_NAME,
                        SECPKG_CRED_INBOUND,
                        NULL,    // LOGON id
                        NULL,    // authentication data
                        NULL,    // get key function
                        NULL,    // get key argument
                        &pAS->_hcred,
                        &Lifetime
                        );
    if (SEC_SUCCESS (ssStatus))
        pAS->_fHaveCredHandle = TRUE;
    else
    {
        fprintf (stderr, "AcquireCredentialsHandle failed: %u\n",
                 ssStatus);
        return(FALSE);
    }
}

// Prepare the output buffer.
OutBuffDesc.ulVersion = 0;
OutBuffDesc.cBuffers  = 1;
OutBuffDesc.pBuffers  = &OutSecBuff;

OutSecBuff.cbBuffer   = *pcbOut;
OutSecBuff.BufferType = SECBUFFER_TOKEN;
OutSecBuff.pvBuffer   = pOut;

// Prepare the input buffer.
InBuffDesc.ulVersion  = 0;
InBuffDesc.cBuffers   = 1;
InBuffDesc.pBuffers   = &InSecBuff;

InSecBuff.cbBuffer    = cbIn;
InSecBuff.BufferType  = SECBUFFER_TOKEN;
InSecBuff.pvBuffer    = pIn;

ssStatus = g_pFuncs->AcceptSecurityContext (
                    &pAS->_hcred,
                    pAS->_fNewConversation ? NULL : &pAS->_hctxt,
                    &InBuffDesc,
                    ASC_REQ_MUTUAL_AUTH,  // context requirements
                    SECURITY_NATIVE_DREP,
                    &pAS->_hctxt,
                    &OutBuffDesc,
                    &ContextAttributes,
                    &Lifetime
                    );
if (!SEC_SUCCESS (ssStatus))
{
    fprintf (stderr, "AcceptSecurityContext failed: %u\n", ssStatus);
    return FALSE;
}
if (!(ContextAttributes && ASC_RET_MUTUAL_AUTH))
    _tprintf(TEXT("Mutual Auth not set in returned context.\n"));

pAS->_fHaveCtxtHandle = TRUE;

// Complete the authentication token -- if necessary.
if ((SEC_I_COMPLETE_NEEDED == ssStatus) ||
                        (SEC_I_COMPLETE_AND_CONTINUE == ssStatus))
{
    if (g_pFuncs->CompleteAuthToken)
    {
        ssStatus = g_pFuncs->CompleteAuthToken (&pAS->_hctxt,
                                                &OutBuffDesc);
        if (!SEC_SUCCESS(ssStatus))
        {
            fprintf (stderr, "complete failed: %u\n", ssStatus);
            return FALSE;
        }
    } else
    {
        fprintf (stderr, "Complete not supported.\n");
        return FALSE;
    }
}

*pcbOut = OutSecBuff.cbBuffer;

if (pAS->_fNewConversation)
    pAS->_fNewConversation = FALSE;

*pfDone = !((SEC_I_CONTINUE_NEEDED == ssStatus) ||
                (SEC_I_COMPLETE_AND_CONTINUE == ssStatus));

return TRUE;
}


Mutual Authentication in RPC Applications

RPC services can use service connection points to publish themselves, or they can use the RPC name service (RpcNs) APIs. This section provides sample code that shows how to perform mutual authentication with an RPC service that publishes itself using the RPC name service (RpcNs) APIs.

The steps for performing mutual authentication in an RPC application are as follows:

To register SPNs in the directory at service installation

  1. Call the DsGetSpn function to compose service principal names (SPNs) for the service.
  2. Call the DsWriteAccountSpn function to register the SPNs on the service account or computer account in whose context the service will run.

To register with the RPC runtime at service startup

Call the RpcServerRegisterAuthInfo function to register the service's SPNs with the RPC authentication service, and specify RPC_C_AUTHN_GSS_NEGOTIATE as the authentication service to use.

To authenticate the service at client startup

  1. Extract the host name from the RPC Binding.
  2. Compose the SPN for the service by calling DsMakeSpn with the service class, the DNS host name, and the service name (the distinguished name of the connection point in the case of RpcNs).
  3. Set up an RPC_SECURITY_QOS structure to request mutual authentication.
  4. Call the RpcBindingSetAuthInfoEx function to set the authentication information for the RPC binding. The client must request at least PKT_INTEGRITY to ensure that communications have not been tampered with. For greater security, the client should specify PKT_PRIVACY to request encryption.
  5. Make the RPC call.

To authenticate the client from the service at the start of each remote procedure call

  1. Call the RpcBindingInqAuthClient function to check the authentication parameters specified by the client. If the client has not requested the desired level of authentication, reject the call. Note that an RPC service must verify the authentication level, authentication service, and client identity on every call to ensure that the client has been properly authenticated.
  2. Call the RpcImpersonateClient function to impersonate the client.
  3. Perform the requested operation.
  4. Call the RpcRevertToSelf function to revert to the service's security context.

The following topics provide sample code for mutual authentication in an RPC application.

  • Client Authentication of an RpcNs Service
  • Composing SPNs for an RpcNs Service
  • How an RpcNs Service Authenticates a Caller

For more information, see the following topics:

  • Publishing with the RPC Name Service (RpcNs)
  • RPC Security Essentials


Client Authentication of an RpcNs Service

The following sample code shows the RPC client's side of a mutually authenticated connection. The client connects to the RPC name service to enumerate the bindings that match the RPC service's interface specification. For each binding handle in the enumeration, the client must call the DsMakeSpn function to compose the corresponding SPN.

void main(void)
{
RPC_STATUS     rpcstatus;
boolean        bResult;
ULONG          ulCode;
TCHAR          szEntryName[] = TEXT("/.:/RpcExampleServiceEntry");
TCHAR          szDsEntryName[MAX_PATH];
BOOL           done=FALSE;
RPC_NS_HANDLE  hNs;  // Context for import operations
TCHAR         *pszBind;
int           ilen;

// Items for ADSI

IADs    *pRoot=NULL;
VARIANT varDSRoot;
HRESULT hr;

// Items for Mutual Auth, SPN

TCHAR    szServiceClass[]=TEXT("RpcExample");
TCHAR    szServiceInstance[MAX_PATH];
TCHAR    szSpn[MAX_PATH];
ULONG    ulSpn = sizeof(szSpn);

RPC_SECURITY_QOS    qos;

// To build an SPN for the RPC service, the DN of the
// service's entry in the RpcServices container is needed. To build
// that DN, retrieve the default naming context and combine it with
// the known strings for the RpcServices container and the service's
// entry in that container.
hr = CoInitialize(NULL);

hr = ADsGetObject(TEXT("LDAP://RootDSE"),
                  IID_IADs,
                  (void**)&pRoot);

hr = pRoot->Get(TEXT("defaultNamingContext"),&varDSRoot);
_tprintf(TEXT("\nDS Root :%s\n"),varDSRoot.bstrVal);

if (pRoot)
    pRoot->Release();

CoUninitialize();

// Compose the DN of the RPC service's connection point using
// "RpcExampleServiceEntry", which is the name of this service's
// entry in the RpcServices container.
_tcscpy(szDsEntryName,
        TEXT("cn=RpcExampleServiceEntry,cn=RpcServices,cn=System,"));
_tcscat(szDsEntryName,varDSRoot.bstrVal);

// Use the service's RPC name service entry to get a handle to
// enumerate the bindings that match our interface specification.
rpcstatus = RpcNsBindingImportBegin(RPC_C_NS_SYNTAX_DCE,
                                     (TCHAR *)&szEntryName,
                                     RpcExample_v1_0_c_ifspec,
                                     NULL,
                                     &hNs);
if (rpcstatus != RPC_S_OK)
    return;

// Loop through the bindings and try each handle until one works.
// In this example, implicit handles aare used: the implicit handle
// "RpcExample_IfHandle" is generated by MIDL and defined in the
// MIDL-generated include file RpcExample.H.
while (rpcstatus != RPC_S_NO_MORE_BINDINGS)
{
    rpcstatus = RpcNsBindingImportNext(hNs,
                                       &RpcExample_IfHandle);
    if (rpcstatus != RPC_S_OK)
        continue;

    // Convert the binding to a string.
    RpcBindingToStringBinding(RpcExample_IfHandle,&pszBind);
    _tprintf(TEXT("String Binding:%s\n"),pszBind);

    // Extract the service name, the host, in this case.
    // Note that the host name used to compose the SPN must be a
    // DNS name. NetBIOS names are not supported.

    ilen=_tcscspn((const TCHAR *)pszBind,TEXT(":"));
    _tcscpy(szServiceInstance,(const TCHAR *)(pszBind+ilen+1));
    RpcStringFree(&pszBind);

    // Set up the authentication info for the call
    //
    // First make the SPN, which is composed of the service class,
    // the DN of the RPC Service object in the DS, and the service
    // instance name, which is the DNS name of the host,
    // which was extracted from the RPC binding string.
    rpcstatus = DsMakeSpn(
        szServiceClass,
        szDsEntryName,     // DN of the entry in RpcServices container
        szServiceInstance, // DNS name of the host for the service
        0,                 // Use the default port
        NULL,              // No referral host
        &ulSpn,            // Size, in bytes, of the szSpn buffer
        szSpn              // buffer to receive the SPN
        );

    _tprintf(TEXT("Client will present SPN %s\n"),szSpn);

    // Set up the RPC_SECURITY_QOS struct for mutual authentication.
    qos.Version             = RPC_C_SECURITY_QOS_VERSION;
    qos.Capabilities        = RPC_C_QOS_CAPABILITIES_MUTUAL_AUTH;
    qos.IdentityTracking    = RPC_C_QOS_IDENTITY_STATIC;
    qos.ImpersonationType   = RPC_C_IMP_LEVEL_IMPERSONATE;

    // Set the authentication information for this binding handle.
    // Specify the service principal name and the QOS information.
    // Ask for PKT_INTEGRITY to ensure that no-one has tampered
    // with the traffic between the client and the authenticated
    // service. Failure to ask for at least PKT_INTEGRITY renders the
    // mutual authentication effectively worthless because an attacker
    // can steal and re-issue compromised packets.
    // For greater security, request PKT_PRIVACY which
    // also encrypts the traffic. PKT_INTEGRITY is a good compromise
    // between security and performance - an attacker can see the
    // traffic but not tamper with it.
    rpcstatus = RpcBindingSetAuthInfoEx(
                       RpcExample_IfHandle,
                       (TCHAR *)szSpn,
                       RPC_C_AUTHN_LEVEL_PKT_INTEGRITY,
                       RPC_C_AUTHN_GSS_NEGOTIATE,
                       NULL,
                       NULL,
                       &qos);

    // Now that mutual authentication parameters have been set,
    // make RPC calls.
    RpcTryExcept
    {
        bResult = Shutdown();
        if (bResult)
            _tprintf(TEXT("Shutdown: Service accepted call\n"));
        else
            _tprintf(TEXT("Shutdown: Service rejected call\n"));

        // Stop looping through the bindings.
        rpcstatus  = RPC_S_NO_MORE_BINDINGS;
    }
    RpcExcept(1)
    {
        ulCode = RpcExceptionCode();
        _tprintf(TEXT("RPC exception 0x%lx = %ld\n"), ulCode, ulCode);
    }
    RpcEndExcept;

    // Free the imported binding
    RpcBindingFree(&RpcExample_IfHandle);
}

// Discard the Import handle.
rpcstatus = RpcNsBindingImportDone(&hNs);

return;
}


Composing SPNs for an RpcNs Service

The following sample code composes the service principal names (SPNs) for an RPC service that has an entry in the RpcServices container in the directory. An RPC service uses the RpcNsBindingExport function to create its RpcServices entry.

An RPC service uses this code to build the SPN or SPNs that identify an instance of the service. The service uses this routine at the following times:

  • To register or unregister the SPNs in the directory, when the service is being installed or removed. For sample code, see Registering the SPNs for a Service.
  • When the service is starting up: to register itself with the RPC authentication service. For sample code, see How an RpcNs Service Authenticates a Caller.

The code uses the distinguished name of the service's RpcServices entry to compose the SPN. So before calling this code, call the RpcNsBindingExport function to create the service's RpcServices entry.

The code calls the DsGetSpn function to build an SPN. The SPN is composed from service's class name and the distinguished name of the service's RpcServices entry.

DWORD
SpnCompose(TCHAR ***pspn, unsigned long *pulSpn)
{
DWORD   status;
TCHAR   szDsEntryName[MAX_PATH];

// Items for ADSI.
IADs    *pRoot=NULL;
VARIANT varDSRoot;
HRESULT hr;

hr = CoInitialize(NULL);

// First get the defaultNamingContext for the local domain.
hr = ADsGetObject(TEXT("LDAP://RootDSE"),
                  IID_IADs,
                  (void**)&pRoot);

hr = pRoot->Get(TEXT("defaultNamingContext"), &varDSRoot);
_tprintf(TEXT("\nDS Root :%s\n"), varDSRoot.bstrVal);

if (pRoot)
    pRoot->Release();

CoUninitialize();

// Compose the DN of the service's entry in the RpcServices container,
// which is created by a call to RpcNsBindingExport.
// The entry for an RPC service is in the System/RpcServices container
// in the defaultNamingContext of the local domain.
_tcscpy(szDsEntryName,
        TEXT("cn=RpcExampleServiceEntry,cn=RpcServices,cn=System,"));
_tcscat(szDsEntryName,varDSRoot.bstrVal);

// Build the SPN for this service using the DN and
// our service's class, "RpcExample".
status = DsGetSpn(
    DS_SPN_SERVICE,     // Type of SPN to create.
    TEXT("RpcExample"), // Service class - a name in this case.
    szDsEntryName,      // DN of the RpcServices for this RPC service.
    0,                  // Use the default instance port.
    0,                  // Number of additional instance names.
    NULL,               // No additional instance names.
    NULL,               // No additional instance ports.
    pulSpn,             // Size of SPN array.
    pspn                // Returned SPN(s).
    );

return status;
}


How an RpcNs Service Authenticates a Caller

For an RPC service, the RPC runtime routine handles most of the details of mutual authentication. When the service starts up, it simply calls the RpcServerRegisterAuthInfo function to register its SPN and specify an authentication service. This example specifies the RPC_C_AUTHN_GSS_NEGOTIATE authentication service which performs the mutual authentication. The service then sets up the RPC bindings and waits for a client connection. When a client binds to the service, the RPC runtime automatically performs the mutual authentication. If the client requests authentication and the client's credentials are invalid, the RPC runtime does not dispatch the client's calls. Note, however, that if the client does not request the proper authentication level, the RPC runtime dispatches the call without performing authentication. For this reason, your service must call the RpcBindingInqAuthClient function to verify the authentication parameters specified by the client.

The following code fragment composes the service's SPN and registers the service for authentication. For the SpnCompose source code, see Composing SPNs for an RpcNs Service.

RPC_STATUS    status;
DWORD         dwStatus;
TCHAR         **pspn;
ULONG         ulSpn=1;

dwStatus = SpnCompose(&pspn, &ulSpn);

// Register with the authentication service
status = RpcServerRegisterAuthInfo((TCHAR*)*pspn,
                                   RPC_C_AUTHN_GSS_NEGOTIATE,
                                   NULL,
                                   NULL);

If the client is authenticated, the service can call the RpcBindingInqAuthClient function to retrieve the security parameters specified by the client. This enables the service to enforce additional security requirements. For example, a service that uses mutual authentication should also require that communications between client and service be digitally signed and/or encrypted. The following code fragment calls the RpcBindingInqAuthClient function and rejects the client's call unless the client has specified the required authentication level.

boolean Shutdown()
{
RPC_STATUS          rpcStatus;
RPC_AUTHZ_HANDLE    hAuth;
TCHAR               *pszSpn;
ULONG               ulAuthnLevel;
ULONG               ulAuthnSvc;
ULONG               ulAuthzSvc;

_tprintf(TEXT("Shutdown called.\n"));

rpcStatus = RpcBindingInqAuthClient(
          0,
          &hAuth,
          &pszSpn,
          &ulAuthnLevel,  // Authentication level requested by client
          &ulAuthnSvc,
          &ulAuthzSvc);

if (rpcStatus != RPC_S_OK) {
    _tprintf(TEXT("RpcBindingInqAuthClient failed: %d\n"), rpcStatus);
    return false;
}

_tprintf(TEXT("Client SPN:%s\n"),pszSpn);
RpcStringFree(&pszSpn);

switch (ulAuthnLevel)
{
    case RPC_C_AUTHN_LEVEL_DEFAULT:
        _tprintf(TEXT("Client Auth Level: DEFAULT\n"));
        return false; // Keep running
    case RPC_C_AUTHN_LEVEL_NONE:
        _tprintf(TEXT("Client Auth Level: NONE\n"));
        return false; // Keep running
    case RPC_C_AUTHN_LEVEL_CONNECT:
        _tprintf(TEXT("Client Auth Level: CONNECT\n"));
        return false; // Keep running
    case RPC_C_AUTHN_LEVEL_CALL:
        _tprintf(TEXT("Client Auth Level: CALL\n"));
        return false; // Keep running
    case RPC_C_AUTHN_LEVEL_PKT:
        _tprintf(TEXT("Client Auth Level: PACKET\n"));
        break;  // Shutdown
    case RPC_C_AUTHN_LEVEL_PKT_INTEGRITY:
        _tprintf(TEXT("Client Auth Level: PACKET_INTEGRITY\n"));
        break;  // Shutdown
    case RPC_C_AUTHN_LEVEL_PKT_PRIVACY:
        _tprintf(TEXT("Client Auth Level: PACKET_PRIVACY\n"));
        break;  // Shutdown
    default:
        _tprintf(TEXT("Client Auth Level: UNKNOWN (%d)\n"),
                               ulAuthnLevel);
        return true;
}

// Shut down.
ReportStatusToSCMgr(
    SERVICE_STOP_PENDING,  // Service state
    NO_ERROR,              // Exit code
    3000);                 // Wait hint

_tprintf(TEXT("\nService shutting down by request.\n"));
RpcMgmtStopServerListening(NULL);
return true;
}


Mutual Authentication in Windows Sockets Applications

Windows Sockets services can use the Registration and Resolution (RnR) APIs to publish services, or they can use service connection points.

For discussion and sample code that shows how to perform mutual authentication for a Windows Sockets service that publishes using a service connection point, see Mutual Authentication in a Windows Sockets Service with an SCP. This example uses an SSPI security package to manage the authentication negotiations between a client and the WinSock service.

Mutual Authentication for Windows Sockets RnR Services

A WinSock RnR service can use similar code to perform mutual authentication using an SSPI package. In this case, the service would compose its SPNs using the distinguished name of the service's entry in the WinsockServices container in the directory.

For example, if the service registers itself with the name "WinSockRnRSampleService", you could compose the service's SPN with the following code. A client could use similar code to construct an SPN to authenticate the service.

DWORD
SpnCompose(TCHAR ***pspn, unsigned long *pulSpn)
{
DWORD   status;
TCHAR   szDsEntryName[MAX_PATH];

// Items for ADSI.
IADs    *pRoot=NULL;
VARIANT varDSRoot;
HRESULT hr;

hr = CoInitialize(NULL);

// First get the defaultNamingContext for the local domain.
hr = ADsGetObject(TEXT("LDAP://RootDSE"),
                  IID_IADs,
                  (void**)&pRoot);

hr = pRoot->Get(TEXT("defaultNamingContext"), &varDSRoot);
_tprintf(TEXT("\nDS Root :%s\n"), varDSRoot.bstrVal);

if (pRoot)
    pRoot->Release();

CoUninitialize();

// Compose the DN of the entry in the WinsockServices container.
_tcscpy(szDsEntryName,
        TEXT("cn=WinSockRnRSampleService,cn=WinsockServices,cn=System,"));
_tcscat(szDsEntryName,varDSRoot.bstrVal);

// Build the SPN for this service using the DN and
// our service's class, "RpcExample".
status = DsGetSpn(
    DS_SPN_SERVICE,     // Type of SPN to create.
    TEXT("RnRExample"), // Service class - a name in this case.
    szDsEntryName,      // DN of the RpcServices for this RPC service.
    0,                  // Use the default instance port.
    0,                  // Number of additional instance names.
    NULL,               // No additional instance names.
    NULL,               // No additional instance ports.
    pulSpn,             // Size of SPN array.
    pspn                // Returned SPN(s).
    );

return status;
}


ADSI Exchange Programmer's Guide

This topic is intended to familiarize you with programmatic access to the Microsoft® Exchange directory service using the Active Directory™ Service Interfaces (ADSI). It contains an introduction to the Exchange directory service and the Exchange schema, and an explanation about how to use ADSI to work with the Exchange directory service and schema. This topic assumes that you have basic knowledge of Visual Basic® and ADSI, as well as of Microsoft Exchange, in general.

Examples are given in Visual Basic. These can easily be adapted to the Microsoft Visual Basic Scripting Edition (VBScript) and used in Active Server Pages or with the Windows Scripting Host.


Active Directory Service Interfaces and LDAP

ADSI is a set of COM interfaces that was created to make it easier for programmers to access disparate directory services. ADSI is built on a provider-based model. Clients use the COM interfaces exposed by ADSI. Providers implement the mapping between those COM interfaces and the underlying directory system. This abstraction saves the programmer from needing to know the API of a particular directory service, such as the Win32 API, for accessing the Windows NT® SAM, the LDAP API, for accessing the Active Directory, or the DAPI, for accessing the Exchange Server directory. Since ADSI is a set of COM interfaces, it can be used within Microsoft Visual C++, Microsoft Visual Basic, or any other COM-capable programming language.

Lightweight Directory Access Protocol (LDAP) is a standard Internet protocol for directory access. It is becoming the standard for directory access and interchange. LDAP is one of the providers included with ADSI. It is a wire protocol, which means that the conversation between an LDAP client and an LDAP server happens over a network. An LDAP client application can access any directory that exposes the LDAP protocol. The most current standard is LDAP version 3. Microsoft Exchange 5.5 supports LDAP version 3.

The following illustration depicts the relationship between these components:


The Exchange Server Directory

The Microsoft Exchange Server directory contains information about mailboxes, distribution lists, servers, topology, and system configuration information. It provides a unified, structured view of all users and resources within the Microsoft Exchange Server environment.

The directory is structured as a hierarchy of objects. Each object contains attributes in which information about that object is stored. Objects may be container objects, in that they contain other objects, or they may be leaf objects, in which case they do not contain other objects. Container objects may contain other container objects.

This hierarchy can be thought of as similar to the file system.

  • The file system has a root, folders which contain files or other folders. The Exchange directory has a root object called the Organization object.
  • Like file system folders, the Exchange directory has containers. These containers can hold sub-containers.
  • In these containers are directory objects, such as mailboxes, distribution lists, servers, or configuration objects.
  • Each object has a set of attributes that are dictated by the object class, named and positioned in accordance with the schema for that object and its container.

The LDAP protocol is natively implemented within the Exchange Server directory service. The LDAP server runs within the DSAMAIN process (DSAMAIN.EXE) on the Exchange Server. LDAP monitors TCP port 389 (or 636 for LDAP over SSL) and accepts incoming LDAP requests. The LDAP server handles these requests and ships the response back to the client. While Microsoft Exchange Server 5.5 supports LDAP version 3, Exchange Server 5.0 supports LDAP version 1.

LDAP version 3 includes many updated features, such as adding, modifying, and deleting directory objects from clients with appropriate permissions. LDAP version 1 was only capable of searching for directory objects and returning attributes.

To take full advantage of all the functionality provided in ADSI, you must have ADSI client programs attach to an Exchange 5.5 Server. This does not mean you need to upgrade all of the Exchange 4.0 and 5.0 servers within your organization before you are able to take advantage of the features of ADSI. Since Exchange Servers within a site have a multi-master directory replication relationship, you can have one Exchange Server in the site that handles LDAP version 3 requests and then replicates the changes to other servers in the site.

For information on configuring the LDAP service for an Exchange Server, refer to the Microsoft Exchange Server 5.5 Books Online.


The Exchange Server Schema

To write ADSI applications that access the Microsoft Exchange directory, it is useful to have an understanding of the Microsoft Exchange directory schema. The directory schema is a collection of descriptions, or rules, that define characteristics of objects in the directory. The schema defines available object classes in the directory, relationships between object classes, attributes of each object class, and specific characteristics of attributes and classes.

To return to the file system analogy, you can have many different types of files on your hard drive. Each file type has inherent qualities and characteristics that govern its format and present its information in ways required by the applications that access it. In the Microsoft Exchange directory these object definitions are called classes. Classes define the characteristics of objects created with a particular class type. You can have many objects in the directory with the same class. Different classes can also be stored in the directory.

Each class describes a set of attributes (or fields of information) that are either mandatory or optional. The class definition includes all the data for the class attributes, including attribute name, length, data type, and so on.

Each mandatory attribute must have a value that is valid in terms of length, data type, and so on. Optional attributes do not need to contain a value. However, if the value is present, it must conform to the length and data type dictated by the object class. Attributes can contain single or multiple values depending on the class definition. For example, the Members field on a Distribution List contains multiple values, the Directory Name field contains a single value.

The full list of object classes and the definition of each class is defined in the schema.

Class definitions are constructed hierarchically. Each derived class inherits all of its parents' attributes. All directory classes are derived from a class called Top.

The Exchange Server schema is not extensible. This means that you cannot add new classes or attributes to the directory. However, Exchange Server does provide you with 15 custom attributes for all directory objects derived from Mail-Recipient. Ten of these attributes are available in Exchange 4.0 and 5.0, and five additional attributes are available in Exchange 5.5. Custom attributes 1 through 10 are 1024 bytes long and can be viewed in the Exchange Administrator application. Custom Attributes 11 through 15 are 2048 bytes long and are only available in Exchange Administrator raw mode, or programmatically. This includes the Exchange Administrator's import and export functions, as well as ADSI.

ADSI is a directory access mechanism. With ADSI, you can access or modify any object in the directory. It is important to differentiate clearly between information that is stored in the Microsoft Exchange directory, and information stored elsewhere. ADSI cannot modify objects in the Exchange Information Stores. Hence, you cannot use ADSI to modify permissions on a public folder, because that information is not in the directory.

Note One special attribute of particular interest to directory programmers is the "Black Link" attribute. This attribute creates an association between the values defined on many objects and a single value on another object. For example, if everyone in the world was allowed to declare one other person in the world as a friend, how could you ever find out who has declared you a friend? You would have to ask everyone in the world who they have defined as their friend and maintain a list. A much more effective solution is to maintain an attribute (in this case containing multiple values) that links to all the people who have declared you as their friend. The Exchange directory keeps track of this sort of information for certain attributes. For example, on each mailbox object there is an attribute containing the name of the mailbox's home server information store (Home-MDB). To find the list of all the mailboxes represented by a directory object (class: MHS-Message-Store), the directory maintains an attribute called Home-MDB-BL. Home-MDB-BL is a property with multiple values containing all of the mailboxes that have declared this store as their home information store.


Exchange Server Administrator Raw Mode

You can see the Exchange schema by using the Exchange Administrator program in raw mode.

To enter raw mode

  • Start the Exchange Administrator program using the "/r" switch from the command line.
    C:\exchsrvr\bin\admin.exe /r

To view the Exchange Server schema

  1. On the View menu, click Raw Directory. A schema container will appear in the left pane of the application.
  2. Click the schema container. Now all classes and attributes in the directory are visible in the right pane. Items with a "C" in the icon are class definitions; items with an "A" in the icon are attribute definitions.

To view an object's raw properties

  1. Highlight an object.
  2. On the File menu, click Raw Properties (or <SHIFT><ENTER>). A dialog box is launched.

    On the left side of this dialog is a list of attributes for this directory object. You can use the "List Attributes of Type" combo box to filter attributes from the list of attributes.

    The "Existing" filter creates a filter to only show attributes that currently have a value.

    The "All" filter will show all attributes.

    The right side of the dialog contains detailed information about the attribute selected in the list box on the left. The information displayed includes the attribute definition and the value or values of the attribute for the current object.

WARNING Using the raw mode of the Exchange Server Administrator program incorrectly can cause serious problems that may require you to reinstall the Microsoft Windows NT Server, and/or the Microsoft Exchange Server. Microsoft cannot guarantee that problems resulting from the incorrect use of raw mode can be solved. Use raw mode at your own risk. Always test changes to the schema on a testing or development Exchange Server.


Advanced Schema Concepts - Important Attributes for Directory Programmers

Every directory attribute contains the following four properties, which are particularly useful to ADSI programmers: Description, Access Category, Heuristics, and Search Flags. The settings of these properties affect the behavior of the attribute on which they exist.


Description - Schema Mapping

LDAP and Exchange Server do not share a common schema. When the LDAP server receives a request for a search to be done for a particular class, such as organizationalPerson, the Exchange directory schema does not have a class called organizationalPerson. This LDAP class needs to be mapped to an Exchange class. The Description field for Exchange classes and attributes holds the LDAP name. Therefore, on the Mailbox class definition in the Exchange Server schema, the Description field holds the string "organizationalPerson". This also applies to attributes. For example, the LDAP attribute sn is mapped to the Exchange Server attribute Last Name. When an Exchange attribute does not have a similar attribute in the LDAP schema, the Exchange name is generally used. For example, there is no attribute in the LDAP schema similar to "NT-Security-Descriptor". Therefore, "NT-Security-Descriptor" is also used for the LDAP name.


Access Category

The Access Category property of an attribute determines what rights a user needs to modify the attribute. The Access Category property value definitions are:

  • 0: Only the system can modify the attribute
  • 1: Users with Modify Admin Attributes permission can modify the attribute
  • 2: Users with Modify User Attributes permission can modify the attribute
  • 3: Users with Modify Permissions rights can modify the attribute

For example, the Exchange Phone Number attribute, which is mapped to the LDAP telephoneNumber attribute, has an Access Category of 2, which means that users with "Modify Users Attributes" permission on the object can change the value. The Primary Windows NT Account associated with a mailbox is granted "Modify Users Attributes" by default, so that the primary user of the mailbox can change the phone number in the directory.

To change an attribute's access category

  1. Double-click the attribute in the schema container.
  2. Select Access-Category from the list box on the left.
  3. Type the new value in the text box.
  4. Click the Set button.

When writing an ADSI application that allows users to change attributes in the Exchange directory, always check or set the Access Category of the attribute.


Heuristics

The Heuristics attribute on a class or attribute determines its visibility in the directory. The Heuristics attribute is on the class or attribute definition in the schema. The Heuristics attribute is a binary bit-mask. Each bit in the binary number is like an on/off switch. Values are shown in the Exchange Administrator as decimal values. To interpret these values you need to convert the values to binary.

Bit | Value: Meaning
Bit 0 | 0: Replicate between sites
 | 1: Do not replicate between sites
Bit 1 | 0: Attribute is not visible through LDAP
 | 1: Attribute is visible to anonymous and authenticated LDAP clients
Bit 2 | 0: Attribute is not accessible by authenticated clients
 | 1: Attribute is accessible to authenticated clients but not anonymous clients
Bit 3 | 0: Attribute is not an operational attribute
 | 1: Attribute is an operational attribute
Bit 4 | 0: Attribute is not visible in Admin UI (Attributes page of "DS Site Configuration" object)
 | 1: Attribute is visible in Admin UI (Attributes page of "DS Site Configuration" object)

For example, the Exchange schema's Last Name attribute (mapped to sn in LDAP) has a heuristics value of 22. The decimal number 22 has a binary value of 10110. Bit 0 is the least significant bit or the bit farthest to the right. In this case, the value is 0. Looking in the table above, we see that if Bit 0 is 0, this attribute is replicated between sites by the Exchange directory replication connector. If we look at bit 1, we see that it is set to 1. Looking in the table we see that if bit 1 is set to 1, this attribute is available to anonymous and authenticated LDAP clients. So this means that the "Last Name" attribute in Exchange is available to LDAP clients.

When writing your Exchange application, always check the visibility of the attribute or class in the schema.


Search Flags

The Search Flags attribute in the Exchange schema defines which attributes are indexed in the Exchange directory. Anyone familiar with database technology knows that by indexing a field in a database, queries execute quicker. Indexing an attribute that is often used in searches can reduce query times from minutes to milliseconds.

The Search Flags value definitions are:

  • 0: Attribute is not indexed and not included in the Outlook Ambiguous Name Resolution process.
  • 1: Attribute is indexed but not included in the Outlook Ambiguous Name Resolution process.
  • 2: Attribute is indexed and included in the Outlook Ambiguous Name Resolution process.

The Exchange attributes in the following table have a Search Flag value of 1.

Exchange Name | LDAP Name
Directory Name | rdn
Hide from AB | Hide-From-Address-Book
USN-Changed | USN-Changed
Primary Windows NT Account | Assoc-NT-Account
Expiration-Time | Expiration-Time
First Name | GivenName
City | L
USN-Intersite | USN-Intersite

The Exchange attributes in the following table have a Search Flag value of 2.

Exchange Name | LDAP Name
Display Name | cn
E-mail Addresses | Proxy-Addresses
Alias Name | uid
Office | physicalDeliveryOfficeName
Last name | sn

If your program requires frequent searching of a particular attribute, then you should set that attribute to be indexed. For example, the Exchange directory does not have an attribute named Employee ID. You would need to use one of the custom attributes. Imagine that everyone's employee ID was inserted into Custom Attribute 1 (mapped to Extension-Attribute-1 for LDAP) on their mailbox object in the Exchange directory. Your application has the employee ID from another source and needs to look up the name of the person that has this employee ID in the Exchange directory. If you were to do this search often, you would want to index Custom Attribute 1.

To create a definition for a custom attribute

  1. Go to the definition of Custom Attribute 1 in the Exchange schema container.
  2. Double-click the Search-Flags attribute in the list box on the left.
  3. The value is 0 by default. Change it to 1.
  4. Click Set.
  5. Restart the Exchange Directory Service. Upon restart, the index is created.

Now Custom Attribute 1 is indexed in the Exchange directory. Searches on the value of this attribute will be performed much faster.

Note Custom attributes are not exposed to the Outlook Client Address Book. To do this, you must customize the Outlook address template using the Exchange Administrator application. Also, if you wish to only access this attribute programmatically, and do not wish to expose it through the default Administrator application, then you should select custom attributes 11 through 15. However, as mentioned above, custom attributes 11 through 15 are viewable through the Exchange Administrator application's raw mode option. They are also viewable using LDAP import and export.


ADs Paths

Just as a file has a fully qualified path in the file system, objects in the directory have a string that represents their exact location within the directory hierarchy. This path is used for binding to the object. Binding is discussed later.

The initial elements of the ADsPath string are the programmatic identifier (ProgID) of the ADSI provider, followed by "://", followed by a path, as dictated by the provider. The path is the server name, followed by the distinguished name of the object. An example of the ADsPath to an Exchange mailbox would be:

"LDAP://MyServer/o=OrgName/ou=SiteName/cn=Recipients/cn=MyMailox"

Another representation for the same object is:

"LDAP://MyServer/cn=MyMailbox,cn=Recipients,o=SiteName,o=OrgName"

Note For the LDAP service provider, the ProgID "LDAP", is case sensitive.

The Exchange LDAP server accepts the LDAP cn attribute in the path as being mapped to the LDAP rdn attribute ("Directory Name" to Exchange). The LDAP cn attribute is actually mapped to the Exchange Display Name attribute. In other words, the "cn=" part of the path shows the value stored in the Exchange Directory Name, but when you ask the LDAP server for cn it gives you the Exchange Display Name. This can be confusing but was done because some systems use an unfriendly value for Directory Name, such as employee ID.


LDAP Error Codes

LDAP error codes can be very helpful in troubleshooting a problem with an ADSI application. There are two ways to find LDAP error codes returned from an Exchange Server.

  1. Perform a network packet trace between the client computer and the server.

    This option can only be used when the LDAP client and the LDAP server are on different machines. With a tool such as the Microsoft Network Monitor, capture all packets between the two machines. Looking at the capture file, you will see the LDAP bind requests and responses, LDAP search requests and responses, and so on. Each response contains an error code. This error code can be found using the LDAP parser included with the Network Monitor.

  2. Increase the diagnostics logging level for the LDAP interface on the Exchange Server.

To increase the diagnostics logging level

  1. In the Exchange Administrator application, highlight the server in the left pane.
  2. In the right pane there should be an object called Directory Service. Double-click this object.
  3. On the Diagnostics Logging tab, set the logging level of the LDAP interface to Maximum.
  4. Look in the application event log for an error event. An event 1354 from the MSExchangeDS should be produced indicating an error number within the description field of the event. The description should look something like: "LDAP search request failed with error: 16." This is the decimal representation of the LDAP error code. The hexadecimal LDAP error code is 0x10.
  5. Once you have the error code, you can look in RPC 2251 or WINLDAP.H for the meaning. For example LDAP error code 0x10 is LDAP_NO_SUCH_ATTRIBUTE.


Limitations of ADSI

A security descriptor can contain two access control lists: a discretionary access-control list (DACL) and a system access-control list (SACL). We are not concerned with the SACL, which is used for auditing. The discretionary access-control list on a directory object defines which Windows NT® accounts have permissions on the particular directory object. The DACL is a list of access-control entries (ACE). Each ACE controls access to an object with an access mask. The ACE has a security identifier (SID) and an access mask that specifies the access rights.

The security descriptor is the data you see displayed on the Permissions tab of an Exchange directory object. The access-control list (ACL) is stored in binary format in the NT-Security-Descriptor attribute of a directory object. ADSI is capable of setting the value of this attribute once it has been created. However, it cannot create the attribute. You must use either the Win32 API or the Exchange SDK AcctCrt COM component.

When creating an Exchange mailbox, you must associate the mailbox with a Windows NT account. This value is stored in the Assoc-NT-Account attribute of the mailbox. This value is the SID, which is the binary representation of the Windows NT account. Again, ADSI is capable of setting the value of this attribute once it's created, but cannot create the attribute from a domain\username pair such as "NORTHAMERICA\jsmith".

To properly obtain the SID of a mailbox or the security descriptor for a mailbox or any other object, you have a few choices.

  • Win32 API. You can use the Win32 API to obtain the SID and security descriptor information.
  • IADsSecurity ADSI Resource Kit. You can use an ADSI extension to the WinNT provider to create an Exchange mailbox.
  • Exchange SDK AcctCrt COM Component. You can use a COM DLL to create Windows NT accounts and security descriptors when creating Exchange mailboxes with ADSI.

See the Visual Basic Create Mailbox example in this topic for a demonstration of how to use the Exchange SDK AcctCrt COM component and the AdsExchUser extension.


Manipulating Security Descriptors and SIDs with the Win32 API

The following example demonstrates how to obtain the SID for a user name, create a security descriptor and convert them to a string of hexadecimal digits.

//////////////////////////////////////////////////////////////////
// Get the SID
//////////////////////////////////////////////////////////////////

LPTSTR pUsername = USERNAME;
LPTSTR pDCName = PDC_NAME;
unsigned long ulSidLen = 0;
LPTSTR pDomain = NULL;
unsigned long ulDomLen = 0;
SID_NAME_USE peUse;
BOOL bSuccess = FALSE;
PSID pSid = NULL;

if (LookupAccountName(pDCName,
                      pUsername,
                      pSid,
                      &ulSidLen,
                      pDomain,
                      &ulDomLen,
                      &peUse) == 0)
{
    pSid = (PSID)new char[ulSidLen];
    pDomain = new wchar_t[ulDomLen];

    if (pSid && pDomain)
    {
        bSuccess = LookupAccountName(pDCName,
                                     pUsername,
                                     pSid,
                                     &ulSidLen,
                                     pDomain,
                                     &ulDomLen,
                                     &peUse);

    }
    delete [] pDomain;
}

// Must convert the binary SID structure to a string of hex digits
BinaryToOctetString((byte *)pSid, ulSidLen, &pSASID);
delete [] pSid;

//////////////////////////////////////////////////////////////////
// Security Descriptor
//////////////////////////////////////////////////////////////////

TRUSTEE ownerTrustee;
TRUSTEE groupTrustee;
EXPLICIT_ACCESS expAccess[1];
SECURITY_DESCRIPTOR *pSD;
ULONG ulSDLen;
LPWSTR strLogin = DOMAIN_USERNAME
SAFEARRAY *pSASD;

ownerTrustee.pMultipleTrustee = NULL;
ownerTrustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
ownerTrustee.TrusteeForm = TRUSTEE_IS_NAME;
ownerTrustee.TrusteeType = TRUSTEE_IS_USER;
ownerTrustee.ptstrName = strLogin;

groupTrustee.pMultipleTrustee = NULL;
groupTrustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
groupTrustee.TrusteeForm = TRUSTEE_IS_NAME;
groupTrustee.TrusteeType = TRUSTEE_IS_GROUP;
groupTrustee.ptstrName = strLogin;

expAccess[0].grfAccessPermissions = ACCESS_MASK; // magic number
expAccess[0].grfAccessMode = SET_ACCESS;
expAccess[0].grfInheritance = NO_INHERITANCE;
memcpy(&(expAccess[0].Trustee), &ownerTrustee, sizeof(TRUSTEE));
expAccess[0].Trustee.ptstrName = strLogin;

if (BuildSecurityDescriptor(&ownerTrustee,
                            &groupTrustee,
                            1, expAccess,
                            0, NULL, NULL,
                            &ulSDLen, (void**)&pSD)==ERROR_SUCCESS)
{
    BinaryToOctetString((byte *)pSD, ulSDLen, &pSASD);
{
else
{
      wprintf(L"Invalid SD");
      return 1;
}
delete [] strLogin;

void BinaryToOctetString(byte* buf, unsigned long ulLen, SAFEARRAY **saOctStr)
{
    unsigned long i;
    SAFEARRAY* pSATemp;
    SAFEARRAYBOUND rgsabound[1];
    char elm[3];
    long dim[1];

    rgsabound[0].lLbound = 0;
    rgsabound[0].cElements = 2 * ulLen;

    pSATemp = SafeArrayCreate(VT_UI1, 1, rgsabound);

    if (pSATemp)
    {
        for (i=0; i < ulLen; i++)
        {
            sprintf(elm, "%02hx", buf[i] );
            dim[0] = i * 2;
            SafeArrayPutElement(pSATemp, dim, elm);
            dim[0] = i * 2 + 1;
            SafeArrayPutElement(pSATemp, dim, &(elm[1]));
        }
        *saOctStr = pSATemp;
    }
}


IADsSecurity and IADsSID from ADSI 2.5 Resource Kit

The IADsSecurity interface allows you to retrieve an Exchange Security descriptor in IADsSecurityDescriptor form. IADsSID allows you to convert many different forms of security identifiers. Using both interfaces, you can create an Exchange Mailbox.


Exchange SDK AcctCrt COM Component

The AcctCrt component was created to be used from languages other than C++ to make the creation of Mailboxes with ADSI easier. This component exposes only one class: AcctMgmt.

The AcctCrt component only allows you to set the security descriptor for the mailbox owner. It does not allow you to set security on objects other than mailboxes or permissions other than those needed for the primary owner of the mailbox. AcctCrt::GenerateSecDescriptor only sets the permissions to 0x1A, which is:

Modify User Attributes | 0x02
Send As | 0x08
Mailbox Owner | 0x10
 | ------
 | 0x1A

Therefore, this tool can only create a mailbox with permissions for the mailbox owner. You cannot manipulate permissions on other objects or set permissions other than mailbox owner.

See AcctCrt COM Component for more information.


Visual Basic Usage Examples

For all Visual Basic® examples you must include a reference to the "Active DS Type Library."

To add a reference to the Active DS Type Library

  1. In the Microsoft® Visual Basic application, from the Projects menu, click References.
  2. Select the checkbox next to Active DS Type Library.

For Visual Basic samples that use ADO, you must also include a reference to "Microsoft ActiveX Data Objects."

To add a reference to the ActiveX Data Objects

  1. In the Microsoft Visual Basic application, from the Projects menu, click References.
  2. Select the checkbox next to Microsoft ActiveX Data Objects.


Creating a Mailbox with IADsSecurityDescriptor and IADsSID in the ADSI Resource Kit

'--------------------------------------------------------
' Security object for SD manipulation
' (REQUIRED ADSI TOOL KIT - REGSVR32 ADSSECURITY.DLL)
'---------------------------------------------------------
Dim sid As New ADsSID 'You can also use -- Set sid = CreateObject("ADsSID") for late binding
Dim sec As New ADsSecurity 'You can also use -- Set sec = CreateObject("ADsSecurity") for late binding

'-------------------------------------
' The rest uses ADSI Interfaces
'-------------------------------------
Dim sd As IADsSecurityDescriptor
Dim dacl As IADsAccessControlList
Dim ace As New AccessControlEntry

'-------------------------------------------------------------------------
' If you don't include the ADSI 2.5 Security Type Library as you make references,
' you must manually declare the following constants.
'-------------------------------------------------------------------------
Const ADS_SID_HEXSTRING = 1
Const ADS_SID_WINNT_PATH = 5
Const ADS_RIGHT_EXCH_MODIFY_USER_ATT = &H2
Const ADS_RIGHT_EXCH_MAIL_SEND_AS = &H8
Const ADS_RIGHT_EXCH_MAIL_RECEIVE_AS = &H10

'--------------------------------------------------------
'---------------CREATING A MAILBOX ----------------------
'--------------------------------------------------------

'--- Server, Org and Site information ---
server = "exchSrv01"
Org = "MICROSOFT"
Site = "INDEPENDENCE"
domain = "INDEPENDENCE"
userName = "jsmith"
password = "passwordHere"

'--- MailBox Parameters ---
strDisplayName = "John Smith"
strFirstName = "John"
strLastName = "Smith"
strAlias = userName
strMTA = "cn=Microsoft MTA,cn=" & server & ",cn=Servers,cn=Configuration,ou=" & Site & ",o=" & Org
strMDB = "cn=Microsoft Private MDB,cn=" & server & ",cn=Servers,cn=Configuration,ou=" & Site & ",o=" & Org
strSMTPAddr = "someone@microsoft.com"

'--- Creating a user to be associated with the mailbox---
Set dom = GetObject("WinNT://" & domain)
Set usr = dom.Create("user", userName)
usr.SetInfo
usr.SetPassword password

'---------------------------------------------------------------
' Build Recipient container's adsPath:
'  LDAP://myserver/CN=Recipients, OU=Site, O=Org
'---------------------------------------------------------------
ADsPath = "LDAP://" + server
ADsPath = ADsPath + "/cn=Recipients,OU="
ADsPath = ADsPath + Site
ADsPath = ADsPath + ",O="
ADsPath = ADsPath + Org

Set objCont = GetObject(ADsPath)

'---Create a new MailBox---
Set mailBox = objCont.Create("organizationalPerson", "cn=" & strAlias)
mailBox.Put "mailPreferenceOption", 0
mailBox.Put "givenName", strFirstName
mailBox.Put "sn", strLastName
mailBox.Put "cn", strDisplayName
mailBox.Put "uid", strAlias
mailBox.Put "Home-MTA", strMTA
mailBox.Put "Home-MDB", strMDB
mailBox.Put "mail", strSMTPAddr
mailBox.Put "MAPI-Recipient", True
mailBox.Put "rfc822Mailbox", strSMTPAddr

'--------------------------------------------------------
' Associating to a primary account
' (Requires the ADSI tool kit - REGSVR32 ADSSECURITY.DLL )
'--------------------------------------------------------
sid.SetAs ADS_SID_WINNT_PATH, "WinNT://" & domain & "/" & strAlias & ",user"
sidHex = sid.GetAs(ADS_SID_HEXSTRING)
mailBox.Put "Assoc-NT-Account", sidHex

' Commit the property cache to the directory service
mailBox.SetInfo

'-------------------------------------------------
' Set the mailbox security
' to allow the user to modify a user attribute,
' send mail, and receive mail
'-------------------------------------------------
Set sd = sec.GetSecurityDescriptor(mailBox.ADsPath)
Set dacl = sd.DiscretionaryAcl
ace.Trustee = domain & "\" & strAlias
ace.AccessMask = ADS_RIGHT_EXCH_MODIFY_USER_ATT Or ADS_RIGHT_EXCH_MAIL_SEND_AS Or ADS_RIGHT_EXCH_MAIL_RECEIVE_AS
ace.AceType = ADS_ACETYPE_ACCESS_ALLOWED
dacl.AddAce ace
sd.DiscretionaryAcl = dacl
sec.SetSecurityDescriptor sd


Deleting an Object

This example demonstrates how to delete a mailbox object (organizationalPerson). To delete a different type of object, simply replace organizationalPerson with the class of the object to be deleted.

Dim objCont As IADsContainer
Set objCont = GetObject(strContPath) 'Bind to the Container
ObjCont.Delete "organizationalPerson", "cn=Username"
ObjCont.SetInfo
Set objCont = Nothing


Creating a Custom Recipient

Dim strDisplayname As String
Dim strAlias As String
Dim strTelephone As String
Dim objCont As IADsContainer
Dim objNewCR As IADs

strDisplayname = "James Smith"
strAlias = "jsmith"
strTelephone = "867-5309"

Set objCont = GetObject("LDAP://Server/cn=Recipients,ou=Site,o=Org")
Set objNewCR = objCont.Create("Remote-Address", CStr("cn=" & stralias))
objNewCR.Put "cn", CStr(strdisplayname)
objNewCR.Put "uid", CStr(stralias)
objNewCR.Put "telephoneNumber", CStr(strtelephone)
objNewCR.Put "Target-Address", "SMTP:jsmith@microsoft.com"
objNewCR.SetInfo


Creating a Recipients Container

Dim objExistingCont As IADsContainer
Dim objNewCont As IADs
' Bind to an existing container
Set objExistingCont = GetObject("LDAP://Server/cn=Recipients,ou=Site")
' Create the container
Set objNewCont = objExistingCont.Create("Container", "cn=MyNewContainer")
' Put the continer-info prop to tell Exchange this is a recipients cont.
objNewCont.Put "Container-Info", &H80000001
objNewCont.SetInfo
Set objCont = Nothing
Set objNewCR = Nothing

After creating a recipients container, you must recalculate the address book hierarchy. The Exchange Administrator application uses standard Windows service manager calls to send a control code to the directory service. The API for accessing the service control manager is documented in the Win32 documentation, but the code used by the Exchange Administrator is not published. The control code needed by the administrator is DS_SERVICE_CONTROL_RECALC_HIERARCHY. The value of this constant is 129.

The following example code uses C++ to issue a hierarchy recalculation in the Exchange directory service. This example can easily be converted to Visual Basic using declarations to access the Win32 functions in advapi32.dll.


Example Code for Recalculating the Hierarchy in the Exchange Directory Service

[C++]

#include <windows.h>

#define DS_SERVICE_CONTROL_RECALC_HIERARCHY ((DWORD) 129)

int main(int argc, char* argv[])
{
    SERVICE_STATUS SrvStat;
    SC_HANDLE hSCM;
    SC_HANDLE hService;

    hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
    hService = OpenService(hSCM,
                "MSExchangeDS",
                SERVICE_USER_DEFINED_CONTROL);

    ControlService(hService,
            DS_SERVICE_CONTROL_RECALC_HIERARCHY,
            &SrvStat);

    if(hSCM != NULL)
        CloseServiceHandle(hSCM);
    if(hService != NULL)
        CloseServiceHandle(hService);

    return 0;
}


Accessing Hidden or Deleted Entries

To access hidden or deleted items, you must use either the OpenDSObject method or the AdsOpenObject function. You must explicitly bind to the directory object with a valid account and use simple clear text authentication with "cn=admin" appended to the username. For example: "cn=Username, dc=Domain, cn=admin". Also, the Exchange service account must be a member of the local administrators group on the server.

If you were to bind to a visible container with this method, and enumerate the objects in the container, you would see the deleted and hidden objects. You can also bind directly to a hidden or deleted object. Without "cn=admin" appended to the username, you would normally receive an error: 0x80072030 "There is no such object on the server. Once you bind with this method, you will be able to access hidden and deleted objects.

Hidden objects are objects with the Boolean attribute, "Hide from AB" ("Hide-From-Address-Book" to LDAP) set to TRUE ("1").

Deleted objects, also called tombstones, are objects with the Boolean attribute "Is-Deleted" (also "Is-Deleted" to LDAP) attribute set to TRUE ("1").

Dim objNS As IADsOpenDSObject
Dim objDeletedObject As IADs

Set objNS = GetObject("LDAP:")
Set objDeletedObject = _
        objNS.OpenDSObject(strADsPath, _
        "cn=Username, dc=Domain, cn=admin",_
    "Password", 1)
'"objDeletedObject" now is a reference to a deleted object


Retrieving Changes

The ability to retrieve changes that have been made to the directory service can be useful for building applications such as directory synchronization tools.

One important attribute is an object's Update Sequence Number or USN. When a change is made to an object in the Exchange directory, the local directory service modifies the USN on the object to be the next available USN for the server. Each server maintains it's own USN count. Therefore, when a server changes an object, the directory service takes the current USN, increments it and stamps it on the changed object. If an application knows the USN number of the last changed object it received from the server, the next time it makes a request for changes it asks for all directory objects with USNs greater than the last USN it received.

Here is a simple example.

Dim ADOConn As ADODB.Connection
Dim ADOCommand As New Command
Dim RS As ADODB.Recordset

Set ADOConn = CreateObject("ADODB.Connection")
ADOConn.Provider = "ADSDSOObject"
ADOConn.Open "Active Directory Provider"

Set ADOCommand.ActiveConnection = ADOConn
ADOCommand.CommandText = "<LDAP://localhost/o=kramerica>;(USN-Changed>1080);rdn;subtree"
Set RS = ADOCommand.Execute

While Not RS.EOF
    Debug.Print (RS.Fields(0))
    RS.MoveNext
Wend

RS.Close
Set ADOConn = Nothing
Set ADOCommand = Nothing
Set RS = Nothing


Getting the Organization and Site Name from a Server

Getting the Organization name from the server is easy. There are two ways to do this:

Dim objRoot As IADs
Set objRoot = GetObject("LDAP://Server")
Debug.Print objRoot.Get("o")

Or

Dim objRoot As IADs
Set objRoot = GetObject("LDAP://Server/rootDSE")
Debug.Print objRoot.Get("defaultNamingContext")

Getting the site in which the server resides requires that you search the entire subtree for a computer object with the server name we are looking for. Once you get the distinguished name of the server, in the form "cn=ServerName, cn=Servers, cn=Configuration, o=SiteName,o=OrgName", parse it out for the site name.

Dim ADOConn As ADODB.Connection
Dim ADOCommand As New Command
Dim RS As ADODB.Recordset
Dim strPath As String

Set ADOConn = CreateObject("ADODB.Connection")
ADOConn.Provider = "ADSDSOObject"
ADOConn.Open "Active Directory Provider"

Set ADOCommand.ActiveConnection = ADOConn
ADOCommand.CommandText = _
"<LDAP://Server>;(&(rdn=ServerName)(objectClass=Computer)) _
;distinguishedName;subtree"
Set RS = ADOCommand.Execute

While Not RS.EOF
    'Store the distinguished name
    strPath = RS.Fields(0)
    'Find the "ou="
    nStart = InStr(strPath, "ou=")
    'Add the length of "ou="
    nStart = nStart + Len("ou=")
    'Find the next comma
    nEnd = InStr(nStart, strPath, ",")
    'The site name is between the "ou=" and the next comma
    Debug.Print Mid(strPath, nStart, nEnd - nStart)
    RS.MoveNext
Wend

RS.Close
Set ADOConn = Nothing
Set ADOCommand = Nothing
Set RS = Nothing


Attributes in a Class

Dim objObject As IADs
Dim objClass As IADsClass
Dim varAttrib As Variant

Set objObject = GetObject _
     ("LDAP://Server/cn=Object,cn=Recipients,ou=Site,o=Orgcn=Object")
Set objClass = GetObject(objObject.Schema)

Debug.Print "Class: " & objClass.Name
Debug.Print "GUID: " & objClass.Guid
Debug.Print "Implemented by: " & objClass.CLSID
Debug.Print "Attributes in this Class: "
Debug.Print "Mandatory Attributes: "

For Each varAttrib In objClass.MandatoryProperties
    Debug.Print vbTab & varAttrib
Next varAttrib

Debug.Print "Optional Attributes: "

For Each varAttrib In objClass.OptionalProperties
    Debug.Print vbTab & varAttrib
Next varAttrib

Set objObject = Nothing
Set objClass = Nothing


Listing the Exchange Server Version of all the Servers in the Organization

Dim ADOConn As ADODB.Connection
Dim ADOCommand As New Command
Dim RS As ADODB.Recordset

Set ADOConn = CreateObject("ADODB.Connection")
ADOConn.Provider = "ADSDSOObject"
ADOConn.Open "Active Directory Provider"

Set ADOCommand.ActiveConnection = ADOConn

ADOCommand.CommandText = _
        "<LDAP://Server>;( objectClass=Computer) _
        ;rdn, serialNumber;subtree"

Set RS = ADOCommand.Execute

While Not RS.EOF
        Debug.Print ("Server: " & RS.Fields(0) & _
        " Version: " & RS.Fields(1))
        RS.MoveNext
Wend

RS.Close

Set ADOConn = Nothing
Set ADOCommand = Nothing
Set RS = Nothing


Creating a Distribution List from the Results of a Query

This example creates a distribution list with the results of a query for all mailboxes (using the organizationalPerson class) whose city attribute (l) is set to "New York".

Dim objRecipients As IADsContainer
Dim objNewDL As IADs
Dim ADOConn As ADODB.Connection
Dim ADOCommand As New ADODB.Command
Dim RS As ADODB.Recordset

Set objRecipients = GetObject(strRecipientsPath)

Set ADOconn = CreateObject("ADODB.Connection")
ADOconn.Provider = "ADSDSOObject"
ADOconn.Open "Active Directory Provider"

strADOQueryString = "<LDAP://Server>;
        (&(objectClass=organizationalPerson)(l=New York));ADsPath;subtree"

Set RS = ADOconn.Execute(strADOQueryString)

'Check to see if any records were found
If Not RS.EOF Then
    'Create a new DL
    Set objNewDL = objRecipients.Create("groupOfNames", "cn=NewDL")

    'Set the props
    objNewDL.Put "cn", CStr(strDisplayname)
    objNewDL.Put "uid", CStr(strAliasName)
    objNewDL.Put "mail", CStr(strSMTPAddr)
    objNewDL.Put "owner", "cn=user,cn=Recipients,ou=Site,o=Org"
    objNewDL.SetInfo

    While Not RS.EOF 'add every mailbox in the RS to the DL
        objNewDL.Add RS.Fields(0).Value
        RS.MoveNext
    Wend

End If

RS.Close
Set ADOConn = Nothing
Set ADOCommand = Nothing
Set RS = Nothing
Set objRecipients = Nothing
Set objNewDL = Nothing

If you want to set the Owner property on the distribution list, which allows a user to change the membership of the distribution list from the Outlook® client, you must set the Owner ("owner" in LDAP) attribute to the distinguished name of the owner's mailbox. You must also set the security descriptor for the distribution list object granting the owner's Windows NT account Modify User Attributes and Send As rights to the object. To do this, you must use either the Win32 API or the ADsSecurity object provided in the ADSI resource kit.

Note When using LDAP calls to an Exchange Server, you can only get the memberOf attribute if you are authenticated as the service account. Additionally, you can get the memberOf attribute on a mailbox only if you are authenticated as the primary Windows NT account of the mailbox. This is to prevent people from doing a query on the memberOf attribute and finding all the distribution lists the user is a member of, including those that have the HIDEDLMEMBERSHIP flag set.


Email Addresses

Exchange directory objects can have multiple email addresses of different types.

Address Type | Exchange Name | LDAP Name
Primary SMTP | E-Mail Addresses for a Mailbox. E-Mail Address for an SMTP Custom recipient (which is also stored in "Target-Address") | mail and rfc822Mailbox
Secondary SMTP | E-Mail Addresses (smtp:) | otherMailbox
X.400 Address | E-Mail Addresses (X400:) | textEncodedORAddress
Other Proxy Addresses | E-Mail Addresses (MS: or CCMAIL:) | otherMailbox

The otherMailbox attribute may have multiple values and contains the address type prefixed to the address.

Note Email address types and addresses in the otherMailbox attribute are separated by a "$". For example: "CCMAIL$User at Post office".

An uppercase address type indicates the Reply To address.

Dim objMailbox As IADs
Dim varAddrs As Variant

Set objMailbox = _
        GetObject("LDAP://Server/cn=Mailbox,cn=Recipients,ou=Site,o=Org")

' Retrieve and print the smtp address
Debug.Print objMailbox.Get("mail")
Debug.Print objMailbox.Get("rfc822Mailbox")

'Retrieve and print the X.400 address
Debug.Print objMailbox.Get("textencodedORaddress")

'Load the other proxy addresses into the property cache
objMailbox.GetInfoEx Array("otherMailbox"), 0
varAddrs = objMailbox.GetEx("otherMailbox")

'Now varAddrs is an array of strings
For i = LBound(varAddrs) To UBound(varAddrs)
    Debug.Print varAddrs(i)
Next I

Set objMailbox = Nothing


Getting or Modifying Configuration Properties

This example displays and sets the value of the maximum message size on an Exchange Server's Message Transfer Agent to 5MB.

Dim objMTA As IADs
Set objMTA = GetObject("LDAP://Server/cn=Microsoft MTA,cn=Server,cn=Servers,cn=Configuration,ou=Site,o=Org")

objMTA.GetInfoEx Array("Deliv-Cont-Length"), 0
Debug.Print objMTA.Get("Deliv-Cont-Length")

objMTA.Put "Deliv-Cont-Length", 5000
objMTA.SetInfo
Set bjMTA = Nothing


Finding the Home Server of a Mailbox

The distinguished name of the home information store for the mailbox is located in the Home-MDB attribute.

Dim objMailbox As IADs
Dim objStore As IADs

Set objMailbox = _
        GetObject("LDAP://Server/cn=Mailbox,cn=Recipients,ou=Site,o=Org")

objMailbox.GetInfoEx Array("Home-MDB"), 0
Debug.Print objMailbox.Get("Home-MDB")

'This distinguished name can be parsed out to obtain the server name.
' Or you can bind to the Home-MDB Path and get the parent object
StorePath = objMailbox.Get("Home-MDB")
Set objStore = GetObject(StorePath)
Debug.Print objStore.Parent

Set objMailbox = Nothing
Set objStore = Nothing

Note Changing the Home-MDB attribute does not move the mailbox. It only breaks the association between the attribute and the mailbox. It is not possible to move a mailbox programmatically using ADSI at this time.


Active Directory Service Interfaces Version 2.5

The Microsoft® Active Directory™ Service Interfaces (ADSI) Software Development Kit (SDK) is a client-side product, based on the Component Object Model (COM), that defines a directory service model and a set of COM interfaces that enables Microsoft Windows NT®/Windows® 2000 and Windows 95 client applications to access several network directory services.

ADSI allows client applications to communicate with directory services that are compliant with LDAP and NDS, in addition to the Windows NT/Windows 2000 Directory Service and namespaces that are compliant with Novell NetWare 3.x.

The ADSI introduction contains the following information:

  • About This Guide
  • What You Should Know Before Reading This Guide
  • Further Reading on ADSI and Related Topics
  • Links to ADSI Code Examples


About This Guide

This document describes the ADSI directory service architecture, defines the Active Directory™ Service Interfaces, and provides examples of ADSI client applications, requirements and overview information for ADSI providers, and an example of an ADSI provider implementation.

Throughout the client application section, examples of writing to Active Directory Service Interfaces or to Active Directory Service Interface objects are provided for several language platforms, notably Visual Basic®, Java, and C/C++. Resources for additional material are listed in the section called Further Reading on ADSI and Related Topics.


What You Should Know Before Reading This Guide

This guide assumes you are familiar with the Component Object Model (COM) and Automation, and that you know how to program in Visual Basic®, Java, or C/C++. For more information on these and other, related technologies, see the section called Further Reading on ADSI and Related Topics.

Some of the terms used in this guide are unique to the ADSI or directory services environments. Other terms will be familiar but may have slightly different meanings in these environments. For a list of ADSI terms and their definitions, see the ADSI Glossary.

This guide includes numerous code examples that demonstrate key features of ADSI and that may prove useful in your own code. For quick access to the code examples, see the Links to ADSI Code Examples page.


Further Reading on ADSI and Related Topics

For more information about Active Directory™ Service Interfaces and the technologies on which they are based, you may find the following sources helpful.

The Active Directory Service Design Specification Version 2.0

The Component Object Model Specification

Chappell, David. Understanding ActiveX and OLE. Redmond, WA: Microsoft Press, 1996.

OLE 2 Programmer's Reference, Volume Two. Redmond, WA: Microsoft Press, 1994.

Brockschmidt, Kraig. Inside OLE, 2nd edition. Redmond, WA: Microsoft Press, 1995.

Rogerson, Dale. Inside COM. Redmond, WA: Microsoft Press, 1997.

Microsoft's OLE DB Programmer's Reference Version 1.0, 1996.

ADSI Functions

ADSI Interfaces

ADSI System Providers


Links to ADSI Code Examples

The following links take you directly to ADSI code examples:

ADsPaths

Examples of ADsPaths.

Binding to an Object

Binding to a directory object (Automation and (C or C++ Examples).

Accessing an Individual Property in the Property Cache

Get and put methods (Automation example).

Get methods (COM vtable example).

Authentication

Use of the OpenDSobject.

Creating a Security Descriptor Object

How to create a security descriptor object (VB example).

Enumerating the ACLs on an ADSI Object

How to enumerate the discretionary ACL (VB example).

ADSI SQL Grammar

Formal grammar for the SQL queries.

ADSI OLE DB Example

Query a directory service.

Using ADO for Querying

User accounts on an ldap server.

Using IDirectorySearch for Querying

Query a directory service..

Automation Examples

Automation - Retrieving a Property From an ADSI Object

Automation - Browsing a Schema Class Definition

Automation - Adding Users to a Group

Automation - Removing Users From a Group

Automation - Listing Children in a Container

Automation - Enumerating Users in a Group

Automation - Creating a User Object

Automation - Deleting a User Object

Automation - Using a Filter

Java - Binding to an ADSI Object.

Query Examples Using ADO

COM Vtable Examples

COM Vtable Examples - Querying a Namespace

COM Vtable Examples - Setting Properties

COM Vtable Examples - Adding Users to a Group

COM Vtable Examples - Removing Users from a Group

COM Vtable Examples - Enumerating Users in a Group

COM Vtable Examples - Creating a User Object

COM Vtable Examples - Deleting a User Object.


ADSI Guides

Active Directory™ Service Interfaces (ADSI) provide the means for client applications of directory services to use one set of interfaces to communicate with any namespace that provides an ADSI implementation. ADSI clients use the well-defined Active Directory Service Interfaces in place of the network-specific API calls to gain simpler access to the services for a namespace.

Active Directory Service Interfaces conform to the Component Object Model (COM) and support standard COM features.

ADSI supplies interfaces that are compliant with Automation, for name-bound controllers like Java, Microsoft® Visual Basic®, and Visual Basic Scripting Edition (VBScript). ADSI can also provide an interface that can optimize performance for interfaces that are not compliant with Automation, to use with language environments like C and C++.

The pure COM interfaces IDirectoryObject and IDirectorySearch support directory queries and object management.

In addition, ADSI supplies its own OLE DB provider, so that any client already using OLE DB, including those using ActiveX Data Objects, can query directory services directly.

Web applications using Active Server Pages also can program access to directory services through ADSI.

ADSI clients can programmatically discover all the ADSI providers at a site and use the same interfaces to communicate with each namespace. As additional providers are installed, the ADSI clients can communicate, without recompiling, with the new namespaces as well.

ADSI supplies system providers for the LDAP, NDS, Novell NetWare 3.x, and WinNT namespaces.


ADSI Programmer's Guide

The following topics are discussed in this guide:

  • Introduction to the ADSI Programmer's Guide
  • Binding to an ADSI Object
  • ADSI Properties and Attributes
  • Accessing and Manipulating Data with ADSI
  • Navigation in Active Directory
  • Searching Active Directory
  • Distributed Query
  • ADSI Extensions
  • Using ADSI with NDS Providers
  • Using ADSI with NWCOMPAT Providers
  • ADSI Utility Interfaces


Introduction to the ADSI Programmer's Guide

The following topics are discussed in this introduction:

  • What are Active Directory Service Interfaces?
  • Multiple Directory Services
  • Who Will Use Active Directory Service Interfaces?
  • Directory Services Today
  • Benefits of Using Active Directory Service Interfaces
  • Active Directory Service Interfaces Architecture
  • Programming Language Support


What are Active Directory Service Interfaces?

Active Directory™ Service Interfaces (ADSI) abstracts the capabilities of directory services from different network providers in a distributed computing environment, to present a single set of directory service interfaces for managing network resources. Administrators and developers can use ADSI services to enumerate and manage the resources in a directory service, no matter which network environment contains the resource.

ADSI makes it easier to perform common administrative tasks, such as adding new users, managing printers, and locating resources throughout the distributed computing environment.

ADSI makes it easy for developers to "directory enable" their applications. Administrators and developers deal with a single set of directory service interfaces — regardless of the installed directory service(s).


Multiple Directory Services

One of the challenges of working within a large, distributed computing environment is identifying and locating resources such as users, groups, print queues, and documents. A directory service is part of a distributed computing environment that provides a way to locate and identify the users and resources available in the system. A directory service is like a phone directory. Given a name for a person or a resource, it provides the information necessary to access that person or resource. You do not have to start with binding information to access a resource on the network.

Most enterprises already have many different directories in place. For example, network operating systems, electronic mail systems, and "groupware" products all have their own directories. Many issues arise when a single enterprise deploys multiple directories. These issues include usability, data consistency, development cost, and support cost, among others.

ADSI addresses these issues by providing a single, consistent, open set of interfaces for managing and using multiple directories.


Who Will Use Active Directory Service Interfaces?

Network Administrators will use ADSI to automate common administrative tasks, such as adding users and groups, managing printers, and setting permissions on network resources.

Independent Software Vendors (ISVs) and end user developers will use ADSI to "directory enable" their products and applications. Services can publish themselves in a directory, clients can use the directory to find the services, and both can use the directory to find and manipulate other objects of interest. Because Active Directory Service Interfaces are independent of the underlying directory service(s), the directory enabled products and applications will operate successfully in multiple network and directory environments.


Directory Services Today

It is common to find a variety of directories — many playing an administrative role — that are deployed within a single organization. These include network resource directories, such as an LDAP-based directory like Active Directory, Microsoft® Windows NT® operating system Directory Service, and Novell NetWare Directory Services, as well as application specific directories, such as Microsoft Exchange. Although a single directory for an entire organization is desirable, no product available today can fill this very large requirement.

The directory challenge

Multiple directories in the organization pose complex challenges to users, administrators, and developers. These problems have limited wide-directory deployment. End users face multiple logons and a variety of interfaces to information across multiple directories. Administrators face the complexity of managing multiple directories. End users and administrators want application developers to use an existing administrative directory, but developers face a dilemma — which one should they use? Each directory offers unique application interfaces. Developers must choose a specific directory implementation, or support multiple versions of their application. As a result, developers seldom use existing directory services.


Benefits of Using Active Directory Service Interfaces

Feature | Benefit
Open | Any directory provider can implement an ADSI provider; users gain freedom of choice in directory services without sacrificing manageability.
DS Independent | Administrative applications are not tightly bound to a given vendor's directory service. Administrative and other directory-enabled applications can be developed with no need to understand vendor-specific directory APIs. The same application can work on multiple directories. Development time and support costs are reduced.
Multiple Language Support | ADSI objects provide easy access to directory services through the Component Object Model. COM applications can be written in languages such as Microsoft® Visual Basic®, Microsoft Visual Basic® Scripting Edition, Java, and C/C++.
Simple Programming Model | ADSI consists of a small, easy to learn set of interfaces.
Scriptable | Any COM Automation controller (for example, Visual Basic, Perl, Rexx, C/C++ and others) can be used to develop directory service applications. Administrators and developers can use the tools they already know. Productivity is enhanced — development time and support costs are reduced.
Functionally Rich | ISVs and sophisticated end users can develop serious applications using the same ADSI models that are used for simple scripted administrative applications.
Extensible | Directory providers, ISVs, and end users can extend ADSI with new objects and functions to add value or meet unique needs.
OLE DB Aware | ADSI provides an OLE DB interface so that programmers familiar with database programming through OLE DB should be productive quickly.


Active Directory Service Interfaces Architecture

This section describes the following topics:

  • Object Model
  • Active Directory Service Interfaces Objects
  • Namespaces
  • Active Directory Service Interfaces Provider


Object Model

The ADSI object model consists of COM objects. Clients manipulate objects with interfaces. ADSI providers implement the objects and their interfaces.


Active Directory Service Interfaces Objects

ADSI objects are COM objects that represent persistent objects in an underlying directory service. An ADSI object is manipulated using one or more COM interfaces.

ADSI objects are divided into two groups: directory service leaf objects, and directory service container objects. A container object can contain other ADSI objects. A leaf object cannot contain ADSI objects.


Namespaces

Objects that reside within a given namespace, are identified by a unique name. For example, files stored on a PC disk drive reside in the file system namespace. The unique name of a file is based on where it is stored in the file system namespace. For example:

C:\public\documents\adsi\adsi_spec.doc

Directory service namespaces also identify the objects they contain by unique names, which are usually based on the location in the directory where the object can be found. For example, in an X.500 directory, a given object might have a name like this:

CN=John,OU=Marketing,O=ArcadiaBay

Different directory services use different forms for naming the objects they contain. This makes dealing with different namespaces challenging, especially for developers, considering all of the different environments on which the code might be running.

A goal of Active Directory Service Interfaces (ADSI) is to provide a naming framework that allows access to namespaces of different directory service providers.

ADSI defines a naming convention that can uniquely identify an object in a heterogeneous environment. These names are called ADsPath strings. ADsPath strings take one of four forms:

"ADs://"

"LDAP://"

"WinNT://"

"NDS://"


Active Directory Service Interfaces Provider

An ADSI provider contains the implementation of ADSI objects and dependent objects for a particular namespace. The following figure demonstrates that clients are concerned only with getting and using interfaces on an object, and not with the details of where and how the software of an object is implemented.


Programming Language Support

You can write ADSI client applications in many languages. For the majority of administrative tasks, ADSI defines interfaces and objects accessible from Automation-compliant languages like Microsoft® Visual Basic®, Microsoft Visual Basic Scripting Edition (VBScript), and Java to the more performance and efficiency-conscious languages such as C and C++.

Smooth integration with Active Server Pages and VBScript make it easy to write Internet applications that access directory services. For integration with OLE DB applications, ADSI supplies an OLE DB provider by supporting a subset of the OLE DB query interfaces. The OLE DB provider supports read-only access to Active Directory.

For Internet applications, using scripting in .asp files (Active Server Pages) can create and manipulate ADSI objects on the server and display the results in a Web page. In Microsoft Management Console, directory-service administration snap-ins can use ADSI to find directory services of interest. In short, Active Directory Service Interfaces can provide access to a broad and diverse set of directory services — including those not yet built.

For access to structures that are not able to be automated, the ADSI architecture defines low-level, interfaces that do not support COM automation that are accessible from languages like C and C++ that are little more than COM wrappers for on-the-wire protocols to a directory service.

Coding to the published interfaces allows your application to reach directory services for all installed ADSI providers and integrate the resulting information. With little or no changes to your code, your application can continue to access additional directory services on your network as new ADSI providers are installed.

The following figure shows how ADSI fits into an application environment. Whether the application is written in Java, Visual Basic, C/C++, VBScript, Microsoft® JScript®, or as a web application using Active Server Pages, Active Directory Service Interfaces provide a clean and easy-to-use access to the underlying directory services without having to use the native network APIs.

Programming language support

As shown in the preceding figure, clients that do not support COM automation have access to all ADSI interfaces, including both pure COM interfaces with the naming convention IDirectoryXXX and Automation COM interfaces with the naming convention IADsXXX. Since clients predominantly request information from directory services, the ADSI flexible query model through OLE DB and IDirectorySearch is especially powerful.


Binding to an ADSI Object

  • Introduction to ADSI Binding
  • Binding String
  • Binding Types Specific to Active Directory
  • Binding Programmatically Using an ADSI Interface
  • Connection Caching


Introduction to ADSI Binding

Binding to an ADSI object is the first important step to communicating with the underlying system. You must bind to an object in order to navigate its namespace, search for specific information, modify the data, or impersonate a user. When specifying binding information, you may optionally supply additional binding characteristics, such as user name, password, server name, encryption methods, and secured authentication. Keep in mind, some providers may implement only partial binding characteristics.


Binding String

Before binding to an object, you must at least know the provider to which you wish to bind. If you know the exact object path, then you can use it along with the provider's name to create a binding string, sometimes referred to as an ADsPath.

ADSI ships with four different providers, as shown in the following table.

Provider Purpose
WinNT:
 For communicating with Windows NT® 4.0 PDCs (Primary Domain Controllers) and BDCs (Backup Domain Controllers).
LDAP:
 For communicating with LDAP servers, including Exchange 5.x directory and Windows® 2000 Active Directory™.
NDS:
 A provider for Novell Directory Services servers.
NWCOMPAT:
 For accessing Novell NetWare servers.

You can use these provider names to access the default provider's namespace. For example, if you bind to LDAP, ADSI binds to a container which contains one object which is the current logged domain object. If you bind to WinNT, ADSI binds to a container holding all domains in the network.

Note The ":"(colon) is part of the standard COM name. The provider name is case sensitive: WinNT is different from WINNT.

To find all providers installed in your computer you can bind to ADs:

Set x = GetObject("ADs:")
For Each provider In x
    provider.Name
Next


WinNT Binding String

The WinNT provider's binding string has the following format:

WinNT:[//DomainName[/ComputerName[/ObjectName[,className]]]]

or

WinNT:[//DomainName[/ObjectName[,className]]]
WinNT:[//ComputerName,computer]

Note [ ] denotes optional parameters; it is not a literal part of the binding string.

If you supply WinNT: as a binding string, ADSI will enumerate all domains in the enterprise.

You can narrow your binding to a specific domain by adding the domain name as part of the binding string. If you would like to communicate with a specific server, you can append a server name in front of a domain name. Optionally, you can directly specify the computer name without specifying a domain name. For example, WinNT://mymachine,computer. If the computer you bind to is not a domain controller, the provider will bind to the local machine accounts.

In addition to a computer name, an object name may be part of a binding string. For example, user, group, printer and service object can all be part of a binding string. Since Windows NT 4.0 allows you to name objects with the same name as long as they are not in the same class, you should avoid the ambiguity by including a class name.

Examples:

ADsPath Meaning
WinNT:
 Bind to the root of the namespace.
WinNT://MyDomain
 Bind to the root of the MyDomain domain
WinNT://MyDomain/bob,user
 Bind to user MyDomain\bob
WinNT://MyDomain/dc01/MyGroup, group
 Bind to a group on the dc01 domain controller
 WinNT://Server01,computer
 Bind to the Server01 computer.


LDAP Binding String

The LDAP provider's binding string has the following format:

LDAP://HostName[:PortNumber][/DistinguishedName]

Where HostName can be a computer name, an IP address, or a domain name.

Like the WinNT case, a server name may be specified in a binding string. Most LDAP servers follow a model requiring the server name to be specified. For example, to bind to an Exchange Server, you must specify an Exchange server name.

If no port number is specified, the LDAP provider uses the default port number, that is, 389. This is useful in some scenarios. For example, an Exchange Server 5.x may be installed on the top of Active Directory™. Since both use 389 as a default port number, you must configure Exchange to use a different port number. Then, the client will be able to talk both Active Directory and Exchange using two different ports on the same machine.

To bind to a specific object, you must specify the distinguished name of the object. A distinguished name (DN) for a given object is guaranteed to be unique.

Examples of binding to a specific object are shown in the following table.

Binding String Meaning
LDAP://dc01
 Bind to the dc01 domain controller
LDAP://exch01.arcadiabay.com
 Bind to an Exchange Server
LDAP://exch01:390
 Bind to an Exchange Server and specify the port number


Binding Types Specific to Active Directory

In addition to the LDAP bindings discussed previously, Active Directory introduces several new concepts for binding to an object.

  • Serverless Binding
  • RootDSE
  • Binding to the Global Catalog


Serverless Binding

Serverless binding means that a specific server is not provided. If, for example, only LDAP is specified in the binding string, the provider will attempt to find and communicate with the best Active Directory domain controller (DC) for the client. Use Windows® 2000 locator services to find the best domain controller.

The following are examples of binding strings:

LDAP:
Bind to the LDAP namespace object. When enumerated, it binds to the best domain controller for the client.
LDAP://DC=ArcadiaBay,DC=MyCompany,DC=com
Bind to the ArcadiaBay domain.
LDAP://CN=Bob, OU=AAA, DC=ArcadiaBay DC=MyCompany,DC=Com
Bind to the Bob object in the AAA organizational unit, in the ArcadiaBay domain.


RootDSE

The RootDSE is a standard attribute defined in the LDAP 3.0 specification. The RootDSE contains information about the binding server. For instance, one interesting piece of information in the RootDSE is the naming context. In Active Directory, the naming context is a unit of replication. Currently, Active Directory defines three naming contexts: Domain, Schema, and Configuration.

Each naming context has a replication scope. If you create an object in the domain naming context, for example, your object will only replicate within the domain. Other naming contexts, such as Schema and Configuration have an enterprise scope replication. This means that every object in the schema is replicated across domains to the entire enterprise.

Consider the following example:

Dim rootDSE as IADs
Set rootDSE = GetObject("LDAP://RootDSE")
domain = rootDSE.Get("defaultNamingContext")
schema = rootDSE.Get("schemaNamingContext")
config  = rootDSE.Get("configurationNamingContext")
Naming Context | Meaning
"LDAP://" & domain | Bind to the root of the domain.
"LDAP://" & schema | Bind to the root of the schema.
"LDAP://" & config | Bind to the root of the configuration.


Binding to the Global Catalog

The global catalog (GC) is a partial and read-only replica for the Active Directory enterprise. The global catalog hosts all objects in the enterprise, however, only selected attributes are replicated to the global catalog. The main purpose of the GC is to serve queries that span multiple domains. A global catalog server is always a domain controller.

To communicate with the global catalog, you need to replace the LDAP: moniker with the GC: moniker. The global catalog listens on port 3268. As with the LDAP provider, if no server is specified, the client will attempt to find the best global catalog. Here is the syntax for binding to the global catalog:

GC:[//ServerName/DistinguishedName]

Examples:

GC:
GC://DC=sales,DC=ArcadiaBay,DC=com


Binding Programmatically Using an ADSI Interface

Now that you are familiar with binding strings, you can learn how to programmatically bind to an object.

ADSI provides two ways to bind to an object:

  • Using ADSI functions to bind directly to an object
  • Using an ActiveX® Data Object (ADO), which internally calls ADSI functions


Using ADSI Functions to Bind Directly to an Object

If you are using Visual Basic, COM provides the GetObject function to retrieve a COM moniker. The GetObject function takes a moniker string. When using ADSI, the binding string is the moniker string. For example:

Dim x As IADs
Set x = GetObject("LDAP://DC=NorthAmerica,DC=ArcadiaBay,DC=com")

The GetObject function will bind to the specified object on behalf of the current user.

If you are using Microsoft® Visual C++®, or other C/C++ compilers, ADSI provides a helper function called ADsGetObject. In addition to the moniker string, this function accepts additional parameters discussed shortly. Alternatively, you can use COM's MkParseDisplayName(Ex) to achieve the same result as GetObject in Visual Basic.

ADsOpenObject

With ADsOpenObject, you have more control over how binding should be achieved. In addition to specifying the user and password, you can specify other flags.

If you are a VB developer, you must use IADsOpenDSObject. For example,

Dim openDS As IADsOpenDSObject
Dim usr As IADsUser

Set openDS = GetObject("LDAP:")

openDS.OpenDSObject("LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
                            "Administrator", "password",
                            ADS_SECURE_AUTHENTICATION)

If you are a C/C++ developer, you should use ADsOpenObject:

IADs    *pObject;
HRESULT hr;
hr = ADsOpenObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
                            L"Administrator", L"password",
                            ADS_SECURE_AUTHENTICATION, IID_IADs,
                            (void**)&pObject);

To bind as the current user, pass NULL in both the user and password parameters.

hr = ADsOpenObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com", NULL,
                   NULL, ADS_SECURE_AUTHENTICATION, IID_IADs,
                   (void**)&pObject);

If you want to communicate with an Active Directory domain controller, you can use three types of user names:

  • Downlevel Logon. For example: ARCADIA\jsmith, or just Jsmith.
  • Distinguished Name. For example: CN=JSmith, OU=Marketing, DC=ArcadiaBay,DC=COM
  • User Principal Name (UPN). For example: jsmith@arcadiabay.com
    hr = ADsOpenObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
                   "CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com"
                   ,"password", ADS_SECURE_AUTHENTICATION, IID_IADs,
                   (void**)&pObject);

The flags you can specify are listed in the following table.

Flag | Meaning
ADS_SECURE_AUTHENTICATION | A secured authentication is requested. You must supply the credentials.
ADS_USE_ENCRYPTION | To use this option, your server must support encryption such as the SSL protocol. For Active Directory, the Certificate Server must be installed to support SSL encryption.
ADS_READONLY_SERVER | For the WinNT provider, it attempts to connect to a PDC (Primary Domain Controller) or a BDC (Backup Domain Controller). For an LDAP provider, this flag is only useful for a serverless binding, and indicates that a writeable server is not required.
ADS_PROMPT_CREDENTIALS | If your SSPI provider honors this flag, it will prompt a credential dialog when authentication process is initiated.
ADS_NO_AUTHENTICATION | No authentication is requested. For an LDAP provider, a connection is established to the server, no binding occurs.


Using an ActiveX Data Object to Bind to ADSI Providers

Since ADSI is also an OLE DB provider, you can use an ActiveX Data Object (ADO) to connect to ADSI providers. As with other ADO providers, to connect to an OLE DB provider, you must create a new connection object and, optionally, specify the credentials. The name of the ADSI OLE DB provider is ADsDSObject.

For example:

Dim con As New Connection
'VBScript use: con = CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"
con.Open "YourDescriptionHere"

In the previous example, you are connected on behalf of the current user. To specify different credentials, use connection properties:

con.Provider = "ADsDSOObject"
con.Properties("User ID") = "jsmith"
con.Properties("Password") = "guesswhat?"
con.Properties("Encrypt Password") = True
con.Open "YourDescriptionHere"

ADSI OLE DB defines the following connection properties.

Property | Data Type | Default
"User ID" | BSTR | NULL
"Password" | BSTR | NULL
"Encrypt Password" | BOOLEAN | FALSE

Note Using OLE DB ADO, you cannot bind to a specific object. You can, however, query a specific object and get a result set back. Only ADSI providers that support IDirectorySearch benefit from having ADO as a programming model.


Connection Caching

Once a connection has been made the connection is cached on the client machine. If you reuse the same server, port, and credentials, ADSI will reuse the same handle. Handles are cached on all objects that have not been destroyed.

For performance benefits, you should keep object handles available and reuse them. If you lose the handle, you must re-bind which causes an unnecessary trip across the network.

Dim cachedConn As IADs
Dim obj As IADs
Dim cachedName As String
Dim objName As String

' Connect to the server and keep this handle around to cache the connection
Set cachedConn = GetObject("LDAP://MyMachine/DC=MyDomain,DC=ArcadiaBay,DC=com")

cachedName = cachedConn.Get("distinguishedName")
Debug.Print (cachedName)

' Reuse the connection to MyMachine opened by cachedConn
'  Note that this line executes very fast because it doesn't
'  have to go over the network again.
Set obj = GetObject("LDAP://MyMachine/CN=Bob,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=com")

objName = obj.Get("distinguishedName")
Debug.Print (objName)

' Release the second connection
Set obj = Nothing

' Reuse the connection to MyMachine opened by cachedConn again
Set obj = GetObject("LDAP://MyMachine/CN=Administrator,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=com")

objName = obj.Get("distinguishedName")
Debug.Print (objName)

' Release the second connection again
Set obj = Nothing

' Release the first connection
Set cachedConn = Nothing

' The connection to MyMachine is now closed


ADSI Properties and Attributes

This section describes the following topics:

  • What are Properties
  • Getting Properties for Active Directory Objects
  • Setting Properties for Active Directory Objects

Introduction

This topic assumes that you have knowledge of ADSI Binding.

Once ADSI binding is established, accessing and manipulating data is perhaps the most common directory operation. A directory may contain thousands of attributes ready to be accessed and manipulated by applications such as administrative tools.

Properties vs. Attributes

When reading directory service documentation, you will often see the terms property and attribute used interchangeably. However, there is a distinction in the case of the ADSI documentation. When we mention a property, we are referring to a data member of an instance of a COM object. When we mention an attribute, we are referring to a value physically stored in the directory. Attributes can become properties by retrieving them from the directory.

In addition, you may see the term property method used. A property method is called to retrieve or store a specific attribute. Property methods usually include the name of the attribute in their name. For example, get_Attribute and put_Attribute.


What are Properties?

Properties are the data members of Active Directory Service Interfaces (ADSI) COM objects. Properties are also referred to as attributes in some documents discussing directory services. In order for an ADSI object to be interesting and useful, you must be able to get and/or set its properties.

For example, given an IADs interface to an object, you might want to get the value of the description property. Properties are named values associated with an object.

Property Cache

The ADSI object model provides a client-side property cache for each ADSI object. Think of the cache as a table in memory that contains the names and values of all an object's properties. When an object is created, its property cache is empty. Calling IADs::GetInfo loads the object's properties from the underlying directory service. If the cache is empty, calling get or get_propertymethod from a client written in C or C++, or calling object.propertymethod from an Automation client, forces an implicit GetInfo. Once the cache is filled, all Get calls work on the contents of the cache only.


Getting Properties for Active Directory Objects

The Get Function

Individual properties can be retrieved from the directory using the IADs::Get function. The following example retrieves a property by name from an object using IADs::Get.

Dim MyUser as IADs
Dim MyCommonName as String

' Bind to a specific user object.
set MyUser = GetObject("LDAP://CN=JamesSmith,OU=MyOrgUnit")

' Get property
MyCommonName = MyUser.Get("CN")

From Automation languages, you can use the property name directly using the dot notation. For example, you can write MyObject.Name or MyObject.Class to access the Name and Class properties defined on IADs. The following example retrieves a property directly from an Active Directory object using the standard IADs interface and the Name property it supports.

Dim MyUser as IADs
Dim MyName as String

' Bind to a specific user object.
Set MyUser = GetObject("LDAP://MyMachine/CN=JamesSmith,DC=ArcadiaBay,DC=COM")

' Get property
MyName = MyUser.Name

You can also use the name of the schema object that describes the property. Pass the name of the property as it is defined in the schema as the first parameter.

Dim MyUser as IADs
Dim MyName as String

' Bind to a specific user object.
set MyUser = GetObject("LDAP://CN=JamesSmith,OU=MyOrgUnit")

' Get property
MyName = MyUser.Get("distinguishedName")

From languages that do not support COM automation, you can also use the IADs::Get method and its related methods as described in the previous example. In addition, you can use the get_propertyname methods supported on all ADSI interfaces.

Here is an example in C++:

IADs       *pUser;
BSTR       bstrName;
HRESULT    hr;

// Bind to user object
hr = ADsGetObject(L"WinNT://MyDomain/Users/John", IID_IADs, (void**)&pUser);

// Get property
if (SUCCEEDED(hr)) {
    hr = pUser->get_Name(&bstrName);

    if (SUCCEEDED(hr)) {
        printf("%S\n", bstrName);
    }

    SysFreeString(bstrName)
}

The GetEx Function

Some properties are returned as multi-valued. They can contain one or more values. For instance, a list of descriptions on a domain is a multi-valued property. You can retrieve a multi-valued property as an array using the IADs::GetEx function.

IADs::GetEx gets single or multi-valued properties in VARIANT structures from the property cache. Finding no value in the cache invokes an implicit IADs::GetInfoEx. IADs::Get can also be used to retrieve single valued properties.

Dim obj As IADs
Dim objList As Variant

Set obj = GetObject("LDAP://MyMachine/CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")

objList = obj.GetEx("description")

For Each Desc In objList
    ' Print the descriptions
    Debug.Print (Desc)
Next

The GetInfo Function

Call GetInfo to refresh all of an ADSI object's cached properties from the underlying directory service. To refresh specific properties, use GetInfoEx.

ADSI invokes an implicit GetInfo if a Get is performed on a specific property in the property cache and no value is found. Once GetInfo has been called, an implicit call will not be repeated. If a value already exists in the property cache, however, calling Get without first calling GetInfo will retrieve the cached value rather than the most current value from the underlying directory. To obtain the most recent values for an object, always call GetInfo. Any changes you have made in the property cache will be replaced with the current values from the server. If you need to preserve your changes on the server you should do a SetInfo to save your changes before you do a GetInfo.

Dim MyUser as IADsUser
'MyUser will be used to demonstrate implicit GetInfo
Dim MyUser2 as IADsUser
'Myuser2 will show the explicit GetInfo

' Bind to a specific user object.
set MyUser = GetObject("LDAP://MyMachine/CN=JamesSmith,DC=ArcadiaBay,DC=COM")
set MyUser2 = GetObject("LDAP://MyMachine/CN=JamesSmith2,DC=ArcadiaBay,DC=COM");

'Perform some time consuming operations…

'Code assumes that the property description has a single value in the directory
' Note that this will IMPLICITLY call GetInfo as at the point this call is made GetInfo
' has not yet been called (implicitly or explicitly) on the MyUser object.
Debug.print "MyUser's description value is "; MyUser.Get("Description")

'Since the GetInfo has already been called implicitly this call is satisfied from
' the value in the cache.
Debug.print "MyUser's sAMAccountName is "; MyUser.Get("sAMAccountName")

' Refresh the cache explicitly to get the most current value
MyUser2.GetInfo

'Perform time consuming operations

'Note that this call is satisfied from the cache has GetInfo has already been called
'explicitly for this object.
Debug.print "MyUser2 has the description set to "; MyUser.Get("Description")

Getting Multi-Valued Properties with IADsPropertyList

Some properties are returned as multi-valued. They can contain one or more values. For instance, a list of descriptions on a domain is a multi-valued property. You can iterate through the list to retrieve the values using IADsPropertyList. The following code example enumerates a list of descriptions and puts them in a standard listbox control.

Dim PropList As IADsPropertyList
Dim Desc As Variant
Dim LDAPPath As String

LDAPPath = "LDAP://MyMachine/DC=MyDomain,DC=MSFT,DC=COM"

' Bind to the object
Set PropList = GetObject(LDAPPath)

' Enumerate the descriptions
For Each Desc In PropList.Description
    ' Print the descriptions
    Debug.Print (Desc)
Next

Optimization Using GetInfoEx

GetInfoEx is called explicitly to refresh some of an ADSI object's cached properties from the underlying namespace. To refresh all the properties, use GetInfo.

GetInfoEx gets specific current values for the properties of an Active Directory object from the underlying directory store, refreshing the cached values.

Dim PropArray As Variant
Dim Prop As Variant
Dim DescList As Variant
Dim obj As IADs

Set obj = GetObject("LDAP://MyMachine")

' Initialize the array of properties to pass to GetInfoEx
PropArray = Array("description", "distinguishedName")

' Make the array a single variant for passing to GetInfoEx
Prop = PropArray

' Get just the description and DN properties
obj.GetInfoEx Prop, 0

DescList = obj.Get("description")

' Enumerate the descriptions
For Each Desc In DescList
    ' Print the descriptions
    Deubg.Print (Desc)
Next

Calling GetInfoEx after changing property values but before calling SetInfo results in losing those changed values. After this call, the cache reflects the property values in the underlying namespace directory store. However, the cache is only updated with the values specifically requested in the GetInfoEx call. Some servers will not return all properties of an object in response to a GetInfo call, you must do an explicit GetInfoEx call naming these "non-default" properties in order to get them into the cache.

Getting Properties with the IDirectoryObject Interface

The IDirectoryObject interface provides clients written in C and C++ with direct access to directory service objects. The interface enables access by means of a direct on-the-wire protocol, rather than through the ADSI property cache. In place of the properties supported by the IADs interface, IDirectoryObject provides methods that support a critical subset of an object's housekeeping methods and provide access to its attributes. With IDirectoryObject, a client can get or set any number of object attributes with one method call. Unlike the corresponding Automation methods, which are batched, those of IDirectoryObject are executed as soon as they are called. Because methods on this interface do not require creating an instance of an Automation directory object, the performance overhead is very low.

Clients written in languages such as C and C++ should call the methods of IDirectoryObject to optimize performance and take full advantage of native directory service interfaces. Automation clients cannot use IDirectoryObject. Instead, they should call the IADs interface.

// IDirObj.cpp : Demonstrates the use of IDirectoryObject.
//

#define _WIN32_DCOM
#define UNICODE
#define _UNICODE

#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "activeds.h"
#include "tchar.h"

void main(void)
{
    IDirectoryObject    *pDirectoryObj;
    DWORD                dwNumAttrToGet;
    DWORD                dwNumAttrGot;
    LPWSTR               AttribNames[10];
    ADS_ATTR_INFO       *pPropEntries;
    HRESULT              hr;

    // Call to initialize COM Library
    CoInitialize(NULL);

    // Bind to a known object asking for the IDirectoryObject Interface.
    hr = ADsOpenObject( L"LDAP://MyMachine/CN=Administrator,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=COM",
                L"Administrator",
                L"",
                ADS_SECURE_AUTHENTICATION,
                IID_IDirectoryObject,
                (void**)&pDirectoryObj);

    if(FAILED(hr)) {
        // Error code goes here.
        printf("ADsOpenObject Failed\n");
        exit(1);
    }

    // Create a list of attributes to get.
    AttribNames[0] = L"distinguishedName";
    dwNumAttrToGet = 1;

    // Call GetObjectAttributes with the
    // list of attributes and a pointer to
    // an array of ADS_ATTRIB_INFO structs.
    hr = pDirectoryObj->GetObjectAttributes( AttribNames,
                                             dwNumAttrToGet,
                                             &pPropEntries,
                                             &dwNumAttrGot);
    if(FAILED(hr)) {
        // Error code goes here.
        pDirectoryObj->Release();
        printf("IDirectoryObject::GetObjectAttributes Failed\n");
        exit(1);
    }

    printf("distinguishedName: %S\n",
            pPropEntries[0].pADsValues->PrintableString);

    // Cleanup.
    pDirectoryObj->Release();
    FreeADsMem(pPropEntries);

    // Call to uninitialize COM Library
    CoUninitialize();

}


Setting Properties for Active Directory Objects

Setting properties is very similar to getting properties. However, when setting a property, the value is only written to the local property cache on the client machine until SetInfo is called. SetInfo is always an explicit call, it is never called implicitly.

You change the property values in the property cache as necessary for your application using IADs methods or IADsPropertyList methods, then write the changes back out to the directory service when they are in a consistent state using SetInfo.

The Put Function

The Put function saves the value for a property for an Active Directory object by name into the property cache. Use IADs::PutEx to save multi-valued properties to the property cache or to remove a property from an object. These values are not persisted to the underlying directory service until IADs::SetInfo is called.

Dim Namespace As IADsOpenDSObject
Dim User As IADsUser
Dim NewName As Variant

Set Namespace = GetObject("LDAP:")

Set User = Namespace.OpenDSObject("LDAP://MyMachine/CN=Administrator,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=COM", "Administrator", "", ADS_SECURE_AUTHENTICATION)

NewName = "John Smith"

' Set using IADs::PutMethod
User.Put "FullName", NewName
User.SetInfo

The PutEx Function

PutEx uses the name of a property to save a single or multi-valued property into the property cache. This overwrites any value currently in the property cache. The values in the cache are not written to the underlying directory service until an IADs::SetInfo occurs. The first argument of PutEx indicates whether you want to replace or add to any existing values for the property. In the following example, any existing values of the description attribute would be erased in the cache when PutEx is called and erased on the server when SetInfo is called.

Dim x As IADs
Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")
'----------------------------------------------
' Assume the otherHomePhoneNumber has the following values:
' 111-1111, 222-2222
'----------------------------------------------
x.PutEx ADS_PROPERTY_APPEND, "OtherhomePhone", Array("333-3333" )
x.SetInfo              'Now the values are 111-1111,222-222,333-3333

x.PutEx ADS_PROPERTY_DELETE, "OtherHomePhone", Array("111-1111", "222-2222")
x.SetInfo              'Now the values are 333-3333
x.PutEx ADS_PROPERTY_UPDATE, "OtherHomePhone", Array("888-8888", "999-9999")
x.SetInfo              'Now the values are 888-8888,999-9999
x.PutEx ADS_PROPERTY_CLEAR, "OtherHomePhone",  vbNull
x.SetInfo              'Now the property has no value

The SetInfo Function

SetInfo saves the current values for the properties for this Active Directory object from the property cache to the underlying directory store. This is analogous to flushing a buffer out to disk.

SetInfo will update objects that already exist in the directory or create a new directory entry for newly created objects.

At the time of the SetInfo call, if any property cache values have been written with a PutEx control code such as ADS_PROPERTY_UPDATE or ADS_PROPERTY_CLEAR, then the appropriate requests are passed on to the underlying directory service.


IADsProperty Family of Interfaces

The IADsProperty family of interfaces consists of IADsPropertyList, IADsPropertyEntry and IADsPropertyValue. These interfaces provide methods to access and manipulate the properties of an object's cache. A property is known as a property entry and corresponds to an attribute defined in the schema. A property entry can have one or many property values. A set of property entries are organized as a property list.

The IADsPropertyList interface manages the property list of an ADSI object. The IADsPropertyEntry interface does that for a property entry. Similarly, the IADsPropertyValue interface represents one or more property values. Together, they provide a mechanism for users to:

  1. Work directly with the property cache.
  2. Work with directories that do not contain schemas, such as an LDAP version 2 server.

The IADsProperty* family of interfaces operate strictly on the property cache and do not make any attempt to cooperate with the server to retrieve or modify the data in the persistent store. As such, this family of interfaces should be used only to examine and manipulate properties in the client's cache. Before using these interfaces, you must call IADs::GetInfo or IADs::GetInfoEx explicitly to load the object's properties into the cache, if the cache has not been initialized. After calling the methods of these interfaces, you must call IADs::SetInfo to persist the changes to the underlying directory store.

The following code snippets provides three examples for how this family of interfaces can be used. The first one is a C/C++ example and the second and third are in Visual Basic.

C/C++ Example

HRESULT hr;
BSTR    bstr;
VARIANT var;
IDispatch *pDispatch;
IADsPropertyList *pPropList;
IADsPropertyEntry *pEntry;
IADsPropertyValue *pValue;
LONG  lADsType;
IADs *pADs=NULL;

//////////////////////////////////////////////////
// Note: For brevity, we omit error handling
////////////////////////////////////////////////////

hr = ADsGetObject(L"LDAP://DC=Sales,DC=ArcadiaBay,DC=Com", IID_IADs, (void**) &pADs );

///////////////////////////////////////////////////////////////////
// Retrieve objects prior to calling IADsProperty* methods
////////////////////////////////////////////////////////////////////
hr = pADs->GetInfo();
hr = pADs->QueryInterface( IID_IADsPropertyList, (void**) &pPropList );
pADs->Release();

////////////////////////////////////////////////////
// Get the 'dc' property
//////////////////////////////////////////////////
hr = pPropList->GetPropertyItem( L"dc", ADSTYPE_CASE_IGNORE_STRING, &var );

if ( SUCCEEDED(hr) )
{
    //////////////////////////////////////////
    // Get the IADsPropertyEntry interface
    //////////////////////////////////////////
    pDispatch = V_DISPATCH( &var );
    hr = pDispatch->QueryInterface( IID_IADsPropertyEntry, (void**) &pEntry );
    VariantClear( &var );

    if ( SUCCEEDED(hr) )
    {
        VARIANT  varValueArray;
        VARIANT  varValue;
        long   idx=0;

        ///////////////////////////////////////////////
        // get_Values return array of VT_DISPATCH
        ////////////////////////////////////////////////
        hr = pEntry->get_Values( &varValueArray );
        hr = pEntry->get_ADsType( &lADsType);
        hr = SafeArrayGetElement( V_ARRAY( &varValueArray ), &idx, &varValue );
        pEntry->Release();  // Release entry

        /////////////////////////////////////
        // IADsPropertyValue
        ////////////////////////////////////////
        pDispatch = (IDispatch*) V_DISPATCH(&varValue);
        hr = pDispatch->QueryInterface( IID_IADsPropertyValue,
                                        (void**) &pValue );
        pDispatch->Release();

        /////////////////////////////
        // Display the value
        /////////////////////////////
        hr = pValue->get_CaseIgnoreString( &bstr);
        printf("%S\n", bstr );
        SysFreeString( bstr );
        pValue->Release();
    }
}

Visual Basic Example 1

Dim propList As IADsPropertyList
Dim propEntry As IADsPropertyEntry
Dim propVal As IADsPropertyValue
Dim sName As String

Set propList = GetObject("LDAP://DC=ArcadiaBay,DC=com")

propList.GetInfo
Set propEntry = propList.GetPropertyItem("dc", ADSTYPE_CASE_IGNORE_STRING)

For Each v In propEntry.Values
    Set propVal = v
    Debug.Print propVal.CaseIgnoreString
Next

Visual Basic Example 2

' VB Sample: Enumerating properties in the property list

Dim propList As IADsPropertyList
Dim propEntry As IADsPropertyEntry
Dim propVal As IADsPropertyValue
Dim count As Long

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")

propList.GetInfo
count = propList.PropertyCount
Debug.Print "No of Property Found: " & count

For i = 0 To count - 1
    'Each item in property list has a property entry
    Set propEntry = propList.Item(i)
    Debug.Print propEntry.Name
    Debug.Print propEntry.ADsType

    'Each value in property entry has property values
    For Each v In propEntry.Values
        Set propVal = v
        Debug.Print propVal.ADsType
    Next

Next

IADsPropertyList, IADsPropertyEntry and IADsPropertyValue

In addition to the IADs methods that get and set individual properties in the cache, ADSI provides the IADsPropertyList, IADsPropertyEntry, and IADsPropertyValue interfaces that allow client software to access the property cache directly. These interfaces allow you to work with the properties without having to use the individual property names.

Interface | Purpose
IADsPropertyList | Manage the entire list of properties defined on an object.
IADsPropertyEntry | Manage individual property attributes and values.
IADsPropertyValue | Read and write values for each property entry in the property cache.

The ADSI property-cache interfaces represent each property entry. IADsPropertyEntry allows access to a single property in the cache and includes a pointer to the list of IADsPropertyValue pointers needed to represent each value.

The IADsPropertyList interface manages a property list node in a namespace. With this interface, you can enumerate the properties in the list, add and remove properties, or purge the entire list in one step.

Dim propList As IADsPropertyList
Dim propEntry As IADsPropertyEntry
Dim propValue As IADsPropertyValue
Dim rootDSE As IADs

'Get the rootDSE entry that contains configuration information
Set rootDSE = GetObject("LDAP://RootDSE")

'Bind to the domain
Set propList = GetObject("LDAP://" & rootDSE.Get("defaultNamingContext"))

'Get the Property Entries

Set propEntry = propList.GetPropertyItem("allowedChildClassesEffective", ADSTYPE_CASE_IGNORE_STRING)

'Get more info about Property Entry
Debug.Print propEntry.ADsType
Debug.Print propEntry.ControlCode
Debug.Print propList.PropertyCount

'Values contain pointers to IADsPropertyValues interfaces
For Each v In propEntry.Values
    Set propValue = v
    Debug.Print propValue.CaseIgnoreString
Next


Accessing and Manipulating Data with ADSI

Once binding has been established, accessing and manipulating data is perhaps the most common directory operation. A directory may contain thousands of attributes ready to be accessed by applications such as administrative tools.

Please see Binding to an ADSI Object for more information on binding.


Single vs. Multiple Valued Attributes

Attributes in the directory may contain single or multiple values. A single attribute can contain NULL or a single value. A multiple valued attribute can contain NULL, a single value, or many values.

Note In Active Directory and most other LDAP servers, the order of a multiple valued attribute is not predetermined. Also, each value in a multiple valued attribute must be unique.

ADSI normally loads schema information if your directory supports a schema, as Active Directory does. Since ADSI knows the syntax of attributes in the schema, you don't need to specify the attribute type when accessing it. ADSI does the hard work for you by marshaling attribute values to the appropriate data type as defined in the schema.

If your directory has no schema, you must supply the data type when you're accessing an attribute.

Note Active Directory, Exchange, Windows NT 4.0, and Site Server all have a schema. In addition, Active Directory has an extensible schema.


Active Directory Operational Attributes

Operational attributes are properties that are stored in and used by the directory for administrative purposes. For example, an operational attribute might be maintained to log the date and time when another attribute has been modified.


Accessing Attributes from Visual Basic and Automation Clients with ADSI

An attribute value can be accessed via IADs::Get or IADs::GetEx. Both functions return a variant value. These functions are only available for directories that support a schema. When using a directory without a schema you must use the IADsProperty family of interfaces to manipulate data.

IADs::Get returns a variant which may or may not be a variant array depending on the number of values returned by the server. For example, if only one value is returned from the server, regardless of whether it is a single or multiple valued attribute, IADs::Get returns a non-variant array. On the other hand, if more than one value is returned, a variant array is returned.

IADs::Get can also return a variant which contains another COM object (via IDispatch).

In other words, IADs::Get always returns one of three types of variant:

  • Simple variant (which may contain VT_BSTR, VT_DISPATCH).
    This is normally set when a single value is returned.
  • Variant array of variants (VT_ARRAY | VT_VARIANT).
    This is normally set when multiple values are returned.
  • Variant array of bytes (VT_ARRAY | VT_UI1).
    This is normally set when binary data is returned.

Note The Visual Basic Scripting Language supports variant and variant array of variants. Hence, VB Script cannot be used to read binary data. The ADSI SDK includes a sample program called the Octet String Converter that allows you to read binary data from VB Script. Visual Basic does not have this limitation.

Example:
'--- Single valued attribute –
Set usr = GetObject("LDAP://CN=jsmith, OU=Sales, DC=ArcadiaBay, DC=Com")
usrname = usr.Get("samAccountName")

'---Multiple valued attribute ---
Set usr = GetObject("LDAP://CN=jsmith, OU=Sales, DC=ArcadiaBay, DC=COM")
homePhones = usr.Get("otherHomePhone")
if ( VarType(homePhones) = vbString Then
    Debug.Print homePhones
Else
    For each phone in homePhones
         Debug.Print phone
    Next
End if

'--- Attribute which turns into a COM object ---
Set ou = GetObject("LDAP://OU=Sales, DC=ArcadiaBay, DC=Com")
Set sd  = ou.Get("ntSecurityDescriptor")
Set acl = sd.DiscretionaryACL

IADs::GetEx always returns a variant array of variants regardless of the number of values returned from the server. This is true when a multiple valued attribute contains single or multiple values.

' --- Multiple valued attribute ---
Set usr = GetObject("LDAP://CN=jsmith, OU=Sales, DC=ArcadiaBay, DC=COM")
homePhones = usr.GetEx("otherHomePhone")
For each phone in homePhones
    Debug.Print phone
Next

You can also use IADs::GetEx for a single valued attribute.

Set usr = GetObject("LDAP://CN=jsmith, OU=Sales, DC=ArcadiaBay, DC=COM")
sds = usr.GetEx("ntSecurityDescriptor")
For each sd in sds
    Set acl = sd.DiscretionaryACL
Next


Accessing Attributes Using C++ and ADSI

Dealing with variant data structures can sometimes be painful in C/C++. You have other choices for accessing directory data. There are two other interfaces for accessing attribute data: IDirectoryObject and IDirectorySearch. You can use IDirectorySearch for retrieving data, but not for modifying it. IDirectorySearch is discussed in detail in the Interfaces topic. Here we will concentrate on the IDirectoryObject interface.

Note IADs::Get and IADs::GetEx are accessible from C/C++, but IDirectoryObject is not available for Visual Basic or Automation clients.

IDirectoryObject::GetObjectAttribute allows you to retrieve both single and multiple valued attributes. GetObjectAttribute takes a list of requested attributes and returns an ADS_ATTR_INFO structure. ADSI allocates this structure but you must free it using FreeADsMem.

The order of returned attribute values is not necessarily the same as the order in which you requested the attributes. Therefore, you must compare the attribute names returned from ADSI.

Note Not all attributes you request will be returned in the ADS_ATTR_INFO structure. Only those attributes with values will be part of the returned structure.

The number of attributes returned is determined by the dwNumberAttributes parameter.

Example:

HRESULT hr;
ADS_ATTR_INFO *pAttrInfo=NULL;
DWORD dwReturn;
LPWSTR pAttrNames[]={L"givenName",L"sn", L"otherTelephone" };
DWORD dwNumAttr=sizeof(pAttrNames)/sizeof(LPWSTR);
IDirectoryObject *pDirObject=NULL;

hr=ADsGetObject(L"LDAP://CN=JohnSmith,OU=Sales,
                DC=ArcadiaBay,DC=com",
                IID_IDirectoryObject,
                (void**) &pDirObject );

hr = pDirObject->GetObjectAttributes(pAttrNames,
                                     dwNumAttr,
                                     &pAttrInfo,
                                     &dwReturn );
if ( SUCCEEDED(hr) )
{
  for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
  {
    if ( _wcsicmp(pAttrInfo->pszAttrName,L"givenName") == 0 )
    {
        printf("First Name: %S\n",
                pAttrInfo->pADsValues>CaseIgnoreString);
    }
    else if ( _wcsicmp(pAttrInfo->pszAttrName, L"sn") == 0 )
    {
        printf("Last Name: %S\n",
                pAttrInfo->pADsValues->CaseIgnoreString);
    }
    else if (_wcsicmp(pAttrInfo->pszAttrName, L"otherTelephone") == 0 )
    {
        // print out multiple valued property, "Other Telephones".
        printf("Other Telephones:\n");

        for (DWORD val=0; val < pAttrInfo->dwNumValues;
                           val++, pAttrInfo->pADsValues++)
        {
            printf(" %S\n", pAttrInfo->pADsValues->CaseIgnoreString);
        }
    }
  }
}

pDirObject->Release();
FreeADsMem( pAttrInfo );


Modifying Attributes from Visual Basic and Automation Clients with ADSI

ADSI provides two methods for modifying attribute data: IADs::Put and IADs::PutEx. These methods modify the data on the client-side cache. You must use IADs::SetInfo to commit the changes.

Note In Active Directory, attribute modifications are transacted. For example, if you modify the sn and givenName attributes, and clear the telephoneNumber attribute of a user object, the changes are committed all at once when you call IADs::SetInfo.

IADs::Put takes an attribute name and a variant parameter. This method can be used to set both single and multiple valued attributes.

IADs::PutEx gives you control over operations on multiple valued attributes. You may append, delete, update and clear existing values. IADs::PutEx always expects a variant array of attribute values. However, you can set a single valued attribute using this method as well.

IADs::PutEx control operation's are:

  • ADS_PROPERTY_CLEAR
    Clear the attribute's value. After the operation, the attribute does not have any value.
  • ADS_PROPERTY_UPDATE
    Replace the current value (or NULL value) with these values. This is the same behavior as IADs::Put.
  • ADS_PROPERTY_APPEND
    Append the existing value with these values. If the current value is NULL, it has the same effect as ADS_PROPERTY_UPDATE.
  • ADS_PROPERTY_DELETE
    Delete the values that match these values. This is different than ADS_PROPERTY_CLEAR. After the delete operation, the attribute may or may not have values.

Examples:

IADs::Put with single value

Dim x As IADs

Set x = GetObject("LDAP://CN=JSmith,CN=Users,DC=ArcadiaBay, DC=Com")

x.Put "givenName", "John"
x.Put "sn", "Smith"

'Commit to the directory
x.SetInfo

IADs::Put with multiple values

Dim x As IADs
Set x = GetObject("LDAP://CN=JSmith,CN=Users,DC=ArcadiaBay, DC=Com")
x.Put "givenName", "John"
x.Put "sn", "Smith"
x.SetInfo    'commit to the directory

IADs::PutEx with multiple values

Dim x As IADs
Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")

'----------------------------------------------
' Assume the otherHomePhoneNumber
' currently has the following values:
' 111-1111,222-2222
'----------------------------------------------

' Set the values to 111-1111,222-222,333-3333
x.PutEx ADS_PROPERTY_APPEND, "OtherhomePhone", Array("333-3333" )
x.SetInfo

' Set the values to 333-3333
x.PutEx ADS_PROPERTY_DELETE, "OtherHomePhone", Array("111-1111", "222-2222")
x.SetInfo

' Set the values to 888-8888,999-9999
x.PutEx ADS_PROPERTY_UPDATE, "OtherHomePhone", Array("888-8888", "999-9999")
x.SetInfo

' Set the property to NULL
x.PutEx ADS_PROPERTY_CLEAR, "OtherHomePhone", vbNullString x.SetInfo

IADs::Put with single values

Dim x As IADs

Set x = GetObject("LDAP://CN=JSmith,CN=Users,DC=ArcadiaBay, DC=Com")

x.PutEx ADS_PROPERTY_UPDATE, "givenName", Array("John")
x.SetInfo    'commit to the directory


Modifying Attributes from C/C++ with ADSI

In addition to IADs::Put, or IADs::PutEx, you may use IDirectoryObject::SetObjectAttributes. To use this method, you must create an ADS_ATTR_INFO structure and set attribute names and values within the structure. You are responsible for allocating and destroying this structure. Second, pass the number of attributes to be modified.

IDirectoryObject::SetObjectAttributes allows you to modify both single and multiple valued attributes. This function gives similar operational controls ( clear, append, delete, and update) to those found in IADs::PutEx. The control constants are:

  • ADS_ATTR_CLEAR
  • ADS_ATTR_ UPDATE
  • ADS_ATTR_APPEND
  • ADS_ATTR_DELETE

Note Like IADs::Put and IADs::PutEx with IADs::SetInfo, the attribute changes are either committed or discarded in Active Directory on a transaction basis.

Example:

HRESULT hr;

IDirectoryObject *pDirObject=NULL;
DWORD dwReturn;
ADSVALUE snValue;

ADS_ATTR_INFO attrInfo[] = {{L"sn",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&snValue,1}};

DWORD dwAttrs = sizeof(attrInfo)/sizeof(ADS_ATTR_INFO);

snValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
snValue.CaseIgnoreString = L"William";

hr = ADsGetObject(L"LDAP://CN=Bruce Johnson,OU=Sales,DC=ArcadiaBay,DC=com", IID_IDirectoryObject, (void**) &pDirObject);

if ( SUCCEEDED(hr) )
{
  hr=pDirObject->SetObjectAttributes(attrInfo, dwAttrs, &dwReturn);
}

pDirObject->Release();


ADSI Attribute Syntax

Each attribute in the directory has an associated syntax. For example, integer, string, numeric, and so on. ADSI defines its own syntax that maps to the native directory syntax. At the end of this section, you will find a table mapping NDS and Active Directory syntax.

Distinguished Name String

Syntax Type: ADSTYPE_DN_STRING

The distinguished name is very useful for linking two objects together. For example, it can be used to link Alice to be Bob's manager. If the Alice object moves to different place, the manager link between Alice and Bob will be updated automatically.

The distinguished name must contain a valid distinguished name object. If the distinguished name does not correspond to a valid existing object, most servers reject the request and give the user a constraint violation error.

Examples:

Set x = GetObject("LDAP://CN=Bob, OU=Sales,DC=ArcadiaBay, DC=com)
x.Put "manager", "CN=Alice, OU=Sales, DC=ArcadiaBay, DC=COM"
x.SetInfo

PADS_ATTR_INFO pInfo;
// .. IDirectoryObject::GetObjectAttribute
printf("%S\n", pInfo->pADsValues->DNString );

Case Exact String and Case Ignore String

Syntax Types: ADSTYPE_CASE_IGNORE_STRING, ADSTYPE_CASE_EXACT_STRING.

Case Exact String is a case-sensitive string while Case Ignore String is a case-insensitive string. A large percentage of attributes in the directory use this syntax.

Note The directory may or may not store this as a Unicode string. However, ADSI accepts and returns wide strings.

Example:

Dim propList As IADsPropertyList
Set propList = GetObject("LDAP://DC=ArcadiaBay,DC=com")
Set propVal = New PropertyValue

'--- Property Value-----
propVal.CaseIgnoreString = "Arcadia Bay, Inc - Seattle, WA"
propVal.ADsType = ADSTYPE_CASE_IGNORE_STRING

Printable String

Syntax Type: ADSTYPE_PRINTABLE_STRING

This syntax is used for string-valued attributes where upper and lower case are considered unequal for comparisons (e.g. "DUNDEE" and "Dundee" do not match). ADSI will accept any contents for a Printable-String; it will not attempt to verify that they are indeed printable.

Numeric String

Syntax Type: ADSTYPE_NUMERIC_STRING

In this syntax, strings match as in Printable String, except that all space characters are ignored in comparisons. ADSI does not do value checking to ensure that only numerals and spaces appear in values of this syntax. Active Directory will accept any content for a numeric string; it will not attempt to verify that the characters are indeed numeric.

UTC Time

Syntax Type: ADSTYPE_UTC_TIME

This syntax stores date and time in a single string. The string format consists of three concatenated parts: (1) YYMMDD; (2) hhmm or hhmmss (both are acceptable); and (3) "Z" to indicate that the time given is coordinated universal time, or "-HHMM" to indicate that the time given is local time which is HHMM behind Greenwich Mean Time (GMT), or "+HHMM" to indicate that the time given is local time which is HHMM ahead of GMT.

Note The first two digits of the year are not stored in this string but can be easily deduced (at least until the year 2080 or so).

Some examples of legal values are "9101311455Z", "910131145503Z", "9101314455-0500", "910131145503+0130". This string is stored as single-byte ASCII characters, and no code page number is stored with it.

Although ordering is supported, it is done only as an ASCII case-insensitive string sort, not by properly interpreting the meaning of the strings.

Any valid string value is accepted. No attempt is made to ensure that the string contains a valid time string.

Example:

sTemp.Format("%02d/%02d/%04d %02d:%02d:%02d", pValues->UTCTime.wMonth, pValues->UTCTime.wDay, pValues->UTCTime.wYear, pValues->UTCTime.wHour, pValues->UTCTime.wMinute, pValues->UTCTime.wSecond );

Boolean

Syntax Type: ADSTYPE_BOOLEAN

Active Directory will only accept a signed 32-bit value for this syntax. It will treat zero as FALSE and all nonzero values as TRUE.

Integer

Syntax Type: ADSTYPE_INTEGER

A 32-bit signed numeric value.

Large Integer

Syntax Type: ADSTYPE_LARGE_INTEGER

A 64-bit signed numeric value. Large integers are actually implemented as a COM objects on the IADsLargeInteger interface. The HighPart and LowPart methods are used to access the two 32 bit halves of the large integer value.

Example:

Dim x as IADsLargeInteger
Set o = GetObject("LDAP://DC=ArcadiaBay,DC=com")
Set x = o.Get("UsnCreated")
Debug.Print x.HighPart
Debug.Print x.LowPart

Octet String

Syntax Type: ADSTYPE_OCTET_STRING

An octet string is returned as a variant array of bytes. This consists of a size count (number of octets) followed by a series of octets. An octet is an 8-bit byte, so a series of octets is a string of binary data.

Object Class

Syntax Type: ADSTYPE_OBJECT_CLASS

Object Class is a unique object identifier for a given schema class. The class of each object instance is identified by the objectClass attribute. Once created, an object's class can never be changed. objectClass is a multiple valued attribute. It lists the specific class of the object, and the classes of all structural or abstract classes from which the specific class was derived. This includes Top, the class from which all other classes are ultimately derived. Active Directory does not list auxiliary classes in the objectClass attribute.

Security Descriptor

Syntax Type: ADSTYPE_NT_SECURITY_DESCRIPTOR

Access rights define what abilities a security principal has when it attempts to perform an operation on an Active Directory object. A security descriptor describes the access control information associated with an object.

The security descriptor is stored as a property of a directory object in the nTSecurityDescriptor property. When an authenticated user attempts to access a directory object, the directory server determines the access granted or denied to the user based on the object's security descriptor.

The ADS_SD_CONTROL_ENUM enumeration specifies control flags for a security descriptor.

Example:

' Gettting a security descriptor
Dim x as IADs
Dim sd as IADsSecurityDescriptor
Dim acl as IADsAccessControlList

Set x = GetObject("LDAP://DC=ArcadiaBay, DC=com")
Set sd = x.Get("ntSecurityDescriptor")

Debug.Print sd.Control
Debug.Print sd.Group
Debug.Print sd.Owner
Debug.Print sd.Revision

Set acl = sd.DiscretionaryAcl
Set sacl = sd.SystemAcl

Mapping Active Directory Syntaxes to ADSI Syntaxes

Attribute Syntax Id | Active Directory Syntax Type | Equivalent ADSI Syntax Type
2.5.5.1 | DN String | DN String
2.5.5.2 | Object ID | CaseIgnore String
2.5.5.3 | Case Sensitive String | CaseExact String
2.5.5.4 | Case Ignored String | CaseIgnore String
2.5.5.5 | Print Case String | Printable String
2.5.5.6 | Numeric String | Numeric String
2.5.5.7 | OR Name DNWithOctetString | Not Supported
2.5.5.8 | Boolean | Boolean
2.5.5.9 | Integer | Integer
2.5.5.10 | Octet String | Octet String
2.5.5.11 | Time | UTC Time
2.5.5.12 | Unicode | Case Ignore String
2.5.5.13 | Address | Not Supported
2.5.5.14 | Distname-Address |
2.5.5.15 | NT Security Descriptor | IADsSecurityDescriptor
2.5.5.16 | Large Integer | IADsLargeInteger
2.5.5.17 | SID | Octet String

Mapping NDS Syntaxes to ADSI Syntaxes

NDS Syntax Id | NDS Syntax Type | Equivalent ADSI Syntax Type
Type1 | DN String | DN String
Type2 | CaseExact String | CaseExact String
Type3 | CaseIgnore String | CaseIgnore String
Type4 | Printable String | Printable String
Type5 | Numeric String | Numeric String
Type6 | CaseIgnore List | Not supported
Type7 | Boolean | Boolean
Type8 | Integer | Integer
Type9 | Octet String | Octet String
Type10 | Telephone Number | Not supported
Type11 | Facsimile Telephone Number | Not supported
Type12 | NDS Network Address | Not supported
Type13 | Octet List | Not supported
Type14 | Email Address | Not supported
Type15 | Path | Not supported
Type16 | Replica Pointer | Not supported
Type17 | Object ACL | Security Descriptor
Type18 | Postal Address | Postal Address
Type19 | Timestamp | Not supported
Type20 | Class Name | Not supported
Type21 | Stream | Not supported
Type22 | Count | Not supported
Type23 | BackLink | Not supported
Type24 | Time | UTC Time
Type25 | Typed Name | Not supported
Type26 | Hold | Not supported
Type27 | Interval | Not supported


Navigation in Active Directory

This section describes the following topics:

  • Enumeration
  • IDirectoryObject
  • Enumeration Helper Functions


Enumeration

In Setting Properties for Active Directory Objects, there were several examples of enumeration. You can also enumerate the children of container objects. These children are objects, themselves rather than just properties on objects. The following example uses the IADsContainer interface and enumerates the children of the container.

Dim MyObject as IADs
Dim Child as IADs
Dim Container as IADsContainer

On Error Resume Next
Set MyObject = GetObject("LDAP://MyServer/DC=MyDomain,DC=ArcadiaBay,DC=com")
Set Container = MyObject

If Err = 0 Then
    For Each Child in Container
        Debug.Print Child.Name
    Next Child
EndIf


IDirectoryObject

Late-bound ADSI clients written in languages such as C and C++ can take advantage of the rich native data types provided by ADSI by calling the IDirectoryObject interface instead of the IADs interface. The IDirectoryObject interface provides methods to support a subset of an object's housekeeping requirements and to access its attributes. The following figure shows the relationships among the data structures:

In the preceding figure, the structure ADS_OBJECT_INFO defines properties that identify the object by distinguished name, relative distinguished name, by container (ParentDN), by object type (ClassDN), and by schema definition (SchemaDN). The attribute descriptor ADS_ATTR_INFO consists of a name, data type, an array of data values shown in ADSVALUE, and a flag that directs the underlying DS to perform certain operations on the attributes detailed in ADS_ATTR_* constants. The data types for these attributes include the ADSI extended syntax types, detailed in ADSTYPE.

Note Although IDirectoryObject provides most of the functionality of IADs, as well as new functionality of its own, it does not currently support ADSI container interfaces. Therefore, all ADSI objects must also implement IADs, which does provide such support.


Enumeration Helper Functions

There are three enumerator helper functions which can be used from C/C++ to aid in the navigation of Active Directory objects. They are ADsBuildEnumerator, ADsEnumerateNext, and ADsFreeEnumerator.

ADsBuildEnumerator

The ADsBuildEnumerator helper function encapsulates the code required to create an enumerator object. It calls the IADsContainer::get_NewEnum method to create an enumerator object and then calls the QueryInterface method of IUnknown to get a pointer to the IEnumVARIANT interface for that object. The enumeration object is the Automation mechanism to enumerate over containers. Use ADsFreeEnumerator to release this enumerator object.

ADsEnumerateNext

The ADsEnumerateNext helper function populates a VARIANT array with elements fetched from an enumerator object. The number of elements retrieved can be smaller than the number requested.

ADsFreeEnumerator

Frees an enumerator object previously created through ADsBuildEnumerator.

Here is an example function using the enumerator helper functions in C++:

/*****************************************************************************
  Function:    TestEnumObject
  Arguments:   pszADsPath - ADsPath of the container to be enumerated (WCHAR*)
  Return:      S_OK
  Purpose:     Example using ADsBuildEnumerator, ADsEnumerateNext and
               ADsFreeEnumerator
******************************************************************************/
#define MAX_ENUM      100

HRESULT
TestEnumObject( LPWSTR pszADsPath )
{
    ULONG cElementFetched = 0L;
    IEnumVARIANT * pEnumVariant = NULL;
    VARIANT VariantArray[MAX_ENUM];
    HRESULT hr;
    IADsContainer * pADsContainer =  NULL;
    DWORD dwObjects = 0, dwEnumCount = 0, i = 0;
    BOOL  fContinue = TRUE;

    hr = ADsGetObject(
                pszADsPath,
                IID_IADsContainer,
                (void **)&pADsContainer
                );

    if (FAILED(hr)) {

        printf("\"%S\" is not a valid container object.\n", pszADsPath) ;
        goto exitpoint ;
    }

    hr = ADsBuildEnumerator(
            pADsContainer,
            &pEnumVariant
            );

    if( FAILED( hr ) )
    {
      printf("ADsBuildEnumerator failed with %lx\n", hr ) ;
      goto exitpoint ;
    }

    fContinue  = TRUE;
    while (fContinue) {

        IADs *pObject ;

        hr = ADsEnumerateNext(
                    pEnumVariant,
                    MAX_ENUM,
                    VariantArray,
                    &cElementFetched
                    );

        if (hr == S_FALSE) {
            fContinue = FALSE;
        }

        dwEnumCount++;

        for (i = 0; i < cElementFetched; i++ ) {

            IDispatch *pDispatch    = NULL;
            BSTR        bstrADsPath = NULL;

            pDispatch = VariantArray[i].pdispVal;

            hr = V_DISPATCH( VariantArray + i )->QueryInterface(IID_IADs, (void **) &pObject) ;

            if( SUCCEEDED( hr ) )
            {
               pObject->get_ADsPath( &bstrADsPath );
               printf( "%S\n", bstrADsPath );
            }
            pObject->Release();
            VariantClear( VariantArray + i );
            SysFreeString( bstrADsPath );
        }

        dwObjects += cElementFetched;
    }

    printf("Total Number of Objects enumerated is %d\n", dwObjects);

    ADsFreeEnumerator( pEnumVariant );

    if (pADsContainer) {
        pADsContainer->Release();
    }

    return(S_OK);

exitpoint:
   return S_OK;
}


Searching Active Directory

This section describes the following topics:

  • Overview
  • Scope and Depth
  • Performance and Handling Large Result Sets
  • Query Syntax
  • Interfaces
  • OLE DB Access with C/C++ and COM


Overview

A major function of Active Directory™ is to resolve queries for information for people, as well as configuration information for machines and services. There are several key areas that you should understand if you want to write the most efficient query to Active Directory. These areas include:

  • Determining the scope of the query. Does the client need to find properties on objects that might be located anywhere within a forest, or only within one domain, or within a given organizational unit (OU)?
  • Determining the depth of the query. Does the query need only go one level or might it cross into other LDAP directories?
  • Performance and handling large result sets. How should the client effectively handle the potential of a large result set?
  • Determining the best queries. What type of queries will provide the most efficient results? What type of queries should the developer stay away from?
  • Understanding the query syntax. ADSI supports both the LDAP syntax as documented in RFC 2254, as well as a subset of SQL.
  • Choice of interfaces. ADSI provides both OLE DB support as well as a C/C++ interface called IDirectorySearch. Since ADSI works for multiple namespaces, you can use these interfaces for querying other namespaces such as NDS and Exchange, as well as Active Directory. Namespaces such as those supported in Windows NT® (WinNT://), prior to Windows® 2000 and NetWare 3, do not support these interfaces, since the underlying infrastructure does not support rich query. Since the ActiveX Data Object (ADO) is a simple scriptable data access object model on top of OLE DB, the OLE DB interfaces work very well for VB programmers and Web page script writers. The new data access capabilities within Visual Studio 6 and Office applications that take advantage of ADO and OLE DB can now access Active Directory information in the same way they access data from other OLE DB providers, such as SQL Server. However, if a C/C++ developer needs to perform a simple directory search, the IDirectorySearch interface might be more appropriate than the OLE DB interfaces.

This section details how to search Active Directory to ensure your program issues the most efficient query, given the requirements of the client.


Scope and Depth

The scope and depth of a query determine the context within the directory. For instance, you can search an entire enterprise, or just a single server.


Scope of Query

The scope of a query is determined by the object to which you bind. If you wish to query across all objects within an enterprise, bind to the global catalog service. Alternatively, you can bind to a specific domain to scope the search to a narrower range.

To identify whether a machine is running the global catalog service

  1. From the Start Menu, go to Admin, Active Directory Sites and select Services Manager.
  2. Select sites, default, first-site name. In servers, select YOURCOMPUTERNAME-ntds settings.
  3. Right-click Select Properties. See if the Global Catalog check box is checked.


Depth of Query

You can specify that your query be restricted to the following limits:

  • Base
  • One level
  • Subtree


Base

A base query limits the search to only the base object. The maximum number of objects returned is always one. This search is useful to verify the existence of an object. For example, if you have an object's distinguished name and you need to verify the object's existence based on the path, you can use a one-level search. If the search fails, you can assume that the object may have been renamed or moved to a different location, or that you were given the wrong information about the object. (Note that you should store the GUID instead of the distinguished name if you wish to revisit an object. This allows the object to be renamed or moved in the directory hierarchy without breaking the persisted link.)


One Level

A one-level search is restricted to the immediate children of a base object, but excludes the base object itself. This setting can perform a targeted search for immediate child attributes of a parent object. For example, if you have a parent object called P1, and its immediate children are: C1, C2, C3, then in a one-level search, C1, C2, and C3 should be included when evaluating the criteria, but P1 would not be part of the search. A one-level search can be used to enumerate all children of an object. In fact, in some ADSI providers, IADsContainer enumeration translates to a one-level search.


Subtree

A subtree search (or a deep search) includes all the objects excluding the base object. This search may generate referrals to other servers. This search has the greatest scope and may return a very large result set. If possible, search on at least one indexed attribute and set the referrals settings (see Performance and Handling Large Result Sets) to match your searching needs. It is also suggested that the results of a subtree search be performed asynchronously and paged to reduce the server overhead and network effectiveness. A subtree search is normally used to search objects for a given scope. For example, search for all users with accounts that will expire in 30 days or less.


Performance and Handling Large Result Sets

Dealing with large result sets can lead to very lengthy response times. This section describes methods for optimizing searches to get the best performance from your directory-enabled application.


What Makes a Fast Query?

There are several "performance tweaking" concepts you should consider when executing a query. The following list identifies the most important ones.

  • If possible, search on indexed attributes only (Check the ADSI documentation for which schema attributes are indexed).Try to use index attributes which you expect will generate the fewest number of hits.
  • Search on objectCategory instead of objectClass since objectClass is not an indexed property.
  • Be aware of referrals. Consider searching the global catalog if your attributes are listed as GC replicated.
  • Stay away from searching for text in the middle and at the end of a string. For example, "cn=*hille*" or "cn=*larouse".
  • Assume a subtree search will return a large result set. Use paging when performing subtree searches. The server will then be able to stream a large result set in chunks reducing the server-side memory resources. This effectively flattens out network usage and reduces the need for sending extremely large chunks of data over the network.
  • Properly scope your searches so as to not retrieve more than is necessary.
  • Perform a complex search on multiple attributes, because it is less performance intensive than performing multiple searches. One search for an object that reads two attributes is cheaper than two searches for the same object, each returning one attribute.
  • For reading group memberships, learn to use range limits to minimize the search size so that you can read a few thousand members at a time.
  • Bind to an object once and hang onto the binding handle for the rest of your session. Do not bind and unbind for each call. If you are using ADO or OLE DB, do not create many connection objects.
  • Read the rootDSE once and remember its contents for the rest of your session.
  • Allow persistent references to objects such as GUIDs, not DNs, so that you can maintain your ability to rename and delete objects. See the binding chapter for more information about binding to a GUID.

Indexing

Whenever possible, search on indexed attributes, or at least one indexed attribute. As in general with databases, searching on an indexed attribute allows the server to more efficiently calculate the result set.

ObjectCategory vs. ObjectClass

Both the objectCategory and objectClass attributes can be used to refer to a given schema class of a directory object. However, there is an important distinction in semantics between the two. (objectClass=foo) refers to such directory objects in which foo represents any class in the object's class hierarchy. (objectCategory=foo), on the other hand, refers to those directory objects in which foo identifies a specific class in the object's class hierarchy. objectClass can take multiple values whereas objectCategory takes a single value and is, thus, better suited for type matching of objects in a directory search. ADSI uses this as the default matching criterion. Searches using one objectClass are not scalable to large databases. ADSI supports (objectCategory=SomeDN) and (objectCategory=Ldap_Display_Name_of_Class), for example, (objectCategory=user). The exception to all of this is that the LDAP search filter (objectClass=*) does not specify a search on object class, but merely tests for the presence of the objects.

Referrals

Optionally, you may enable the option to chase referrals. Referrals happen when the server you are querying does not contain that information, but knows where to find it. The target server returns the result set, which may include both the actual data and a referral to another server to retrieve the additional data. By enabling chase referrals, the underlying ADSI client code will use that referral information to retrieve the target object from the server described in the referral information. Be aware that the disabling of chase referrals may result in a smaller result set, whereas enabling chase referrals may cause a query to span many servers. The best solution to alleviate this overhead is to use the global catalog whenever possible.

For example, when a client asks Server A (A) to query a user object (U), A can suggest that the client continue the search on Server B (B) if U does not reside on A, but is known to be on B. The client has the choice of pursuing the referral or not. Search referrals free the client from requiring advanced knowledge of the capability of each server. However, the client must specify the type of referrals a server should make.

Active Directory offers search referral services. A client can choose any of the following four types of referral chasing:

  • Never. The server should not generate a referral to a client even though it knows that another server holds the requested information.
  • External. The server should generate referrals if the request can be resolved on another server of a different directory tree. For example, a client queries OU=Sales, DC=ArcadiaBay, DC=COM on the msft01 server on the microsoft.com domain. However, the object does not belong to msft01, but is known to be on the arc01 server on the ArcadiaBay.com domain. Thus, msft01 will refer the client to arc01.
  • Subordinate. The server should generate referrals if the request can be resolved on a server whose name forms a contiguous path from the originating server. The search scope must be at the subtree level.

For example, Server A contains objects in DC=Sales, DC=ArcadiaBay, DC=Com. Server B contains objects in DC=Seattle, DC=Sales, DC=ArcadiaBay, DC=Com. Notice that the name of Server B forms a contiguous path from Server A. When a client contacts Server A, requests a subtree search on DC=Sales, DC=ArcadiaBay, DC=Com, and specifies the referral to be a subordinate type, the following event occurs:

Server A returns all objects that it knows within its scope.

Server A informs the client that objects in DC=Seattle, DC=Sales, DC=ArcadiaBay, DC=COM can be found on Server B.

The client can choose to contact Server B. If so, the following event occurs:

Server B responds with the requested objects.

If Server B detects other servers on the contiguous naming path and the process continues.

Always. The server generates referrals if the search can be resolved based on either the external type or the subordinate type.

Note In Active Directory, the global catalog contains all objects in a given enterprise. Searching a global catalog server yields better performance than pursuing referrals from one server to another.

Assembling Query Strings

In Active Directory, using more specific matching criteria tends to boost search performance. This is because Active Directory evaluates all predicates, identifies the indices, and then picks one index that it considers most likely to yield the smallest set of returned values.

When possible, query on indexed attributes. Stay away from searching for text in the middle and on the end of a string. For example, "cn=*hille*" or "cn=*larouse".

Paging

Paging is set by defining how many rows the server calculates before the data is returned to the client over the network

By default, the server responding to a query request will completely calculate a result set before returning the data. In the case of a large result set, this ties up server memory as the result set is being acquired, and network bandwidth when the large result is returned. Setting a page size allows the server to send the data in pages as the pages are being built.. The client will then cache this data and provide a cursor to the application level code. Be aware that even though a page size may be in effect, if a sort order is defined, the server must completely calculate the result set before returning the data to the client.

Paged searching offers benefits to both the client and the server. For instance, the client can be more responsive in presenting the results to end users. This is especially relevant to graphical user interface tools that can begin displaying data while another thread concurrently receives more from the server.

On the server side, paged searching makes the operation scalable. Suppose that one hundred clients issue search requests simultaneously and, on the average, each client gets back two hundred objects. If no page size is specified, the server must have sufficient memory to hold 20,000 objects. On the other hand, if each client were to specify a page size of ten objects, the memory requirements on the server would be reduced by a factor of 20.

Note Not all directory servers support paged searches. Active Directory is one that takes advantage of page size architecture.

Many directory servers specify an Administrative Limit for the maximum number of objects they can return if a client does not specify the page size. When the Administrative Limit is reached, ADSI generates the ERROR_DS_ADMIN_LIMIT_EXCEEDED Win32 error.

In a paged search, a client can abandon the operation in progress. In contrast, in a non-paged search, the client is blocked until the data is completely returned, or an error occurs. This could potentially put a heavy burden on the network.

On behalf of the client, ADSI handles the page size transparently. The client does not have to count the number of objects in progress. ADSI encapsulates the server interaction for the client. From the client's perspective, the search comes back with a complete result set.

Search Attributes Only

Sometimes you do a search to determine what type of information is available for a particular object. In this case, you are only interested in the attributes, not the values, of the object. To accomplish this, you set the search attributes only option. Specifying this option will return the attribute names without their values. However, the result set includes only those attributes that have values assigned. For example, consider an object with the following attributes:

First Name = John
Last Name = Smith
Department = <Empty>
Phone Number = (425) 432-3442

When the search attributes only option is set, the result set includes:

First Name
Last Name
Phone Number

Search Size Limit

To reduce memory requirements, a client can focus on a small number of objects returned from the server and ignore the rest of the result set. To accomplish this, the client specifies a search size limit and other appropriate search criteria. For example, if a directory stores the SAT scores of a school district, you can query the top ten students with the highest SAT scores by specifying a size limit of ten and a descending sort order.

Search Time Limit

When you request a search on a server that is under a heavy workload, you may want to tell the server to restrict the search to a specified time limit. For example, you want to execute an application to generate a weekly report on a server that is running near its capacity. To avoid maximizing the CPU time and preventing other jobs from running, you can set the search time to a small value and rerun the application later if it fails to generate the report.

Some servers might impose their own administrative time limit. In these cases, if you specify a search time limit value greater than the administrative time limit, the server will ignore your specification and use its internal administrative time limit instead.

Search Time Out

A client can also impose a time limit that it is willing to wait for a server to return the result set. The value of the search time out property specifies this limit. When the server fails to respond to a query within the specified time period, the client can abandon the search and try again later.

The time-out property is useful when a client requests an asynchronous search. In an asynchronous search, the client makes a request and then proceeds with other tasks while waiting for the server to return the results. It is possible that the server can go offline without notifying the client. In this case, the client will have no way of knowing whether the server is still processing the query, or if it has ceased to be live. The time-out property gives the client some control of situations like this.

Asynchronous Searches

Generally speaking, enabling asynchronous searches produces the following result: a call to GetFirstRow or the first call to GetNextRow will block until the first entry is returned from the server. This means that if the search on the server is taking a lot of time, you can get back the first few results quickly. This can help if you are, for instance, populating a list box with the search results. You can show the search results and grow the list as they come.

If async is not enabled, the first call to GetFirstRow or GetNextRow will block until the server computes the entire result set and returns. Only then is the first row returned. This is not recommended if the result set is expected to be large.

If both paging and async are enabled, GetFirstRow or GetNextRow will block until the server generates and sends the first page of results. If you have set a reasonable page size you should start seeing results immediately. More importantly, if the search results are expected to be huge, and you are looking for a particular entry, you need not ask for more results from the server after you have found the entries interesting to you.

A paged async search will give you more fine-grained control over how the search should behave. You can opt for this if the search result might be huge and the search might take a long time on the server.


Processing a Result Set

A result set is returned as a series of rows. Each row may or may not contain a given attribute. If you are using the OLE DB provider, the result set appears as would a normal SQL result set. If you are using ADO for the query, the ADODB.Recordset object is used for traversing the result set. IDirectorySearch (available only from languages which support vtable bindings) contains members for moving the row cursor and checking property values in a given row. Alternatively, you can use IDirectoryObject to easily get and set properties.

Caching the Result (Client Side)

Client-side caching stores the result set in the client's memory, providing performance enhancements on the client side. Clients can revisit an object again and again without making separate trips to the server. By default, ADSI caches the result set on behalf of clients. This allows ADSI to provide clients with SQL-like cursor support. You may iterate through a given result set several times. For example, you can safely use IDirectorySearch::GetNextRow and IDirectorySearch::GetPreviousRow. (There will be more discussion on these methods later.)

ADSI also allows clients to turn off the cache to reduce memory requirements. If client-side caching is disabled, the client will not maintain the result set in memory. Each row will be freed after the application retrieves it. Disabling client-side caching can be useful for decreasing the client-side memory requirements in such cases as when you only intend to make a single pass through the result set. However, when clients elect not to cache the result, they give up cursor support.

Sorting

A client can request a server to sort the result set. You can specify:

  • Sort order. Either ascending or descending sort order can be specified.
  • Sort attributes. Multiple attributes can be specified in a sort string. For example, sort by Last Name, then First Name.

When you specify sort order, you should try to use one of the indexed attributes. Otherwise, the server must calculate a complete result set before sending it to the client. This is true even if you have requested a paged search and specified a page size.

Note Not all directory servers support multiple attribute sorts or sort order.


Query Syntax

This section describes the various languages and methods you can use to query the Active Directory, including, SQL, LDAP, COM search interfaces, ADO and OLE DB.


Search Filter Syntax

Search filters allow you to define search criteria and give you better control to achieve more effective and efficient searches. For example, you may be interested in all the users whose surname is "Smith". Or you may want to find out all the team members reporting to the manager named "Mary Jane". Search filters help you to manage complex queries more easily.

ADSI supports the LDAP search filters as defined in RFC2254. These search filters are represented by Unicode strings. Some examples of LDAP search filters are as follows:

(objectClass=*)
All objects
(&(objectClategory=person)(objectClass=user)(!cn=andy))
All user objects but "andy"
(sn=sm*)
All objects with a surname that starts with "sm"
(&(objectCategory=person)(objectClass=contact)(|(sn=Smith)(sn=Johnson)))
All contacts with a surname equal to "Smith" or "Johnson"

These search filters use one of the following formats:

<filter>=(<attribute><operator><value>)

or

(<operator><filter1><filter2>)

Some operators that are frequently used for search filters are listed in the following table:

= | Equal to
~= | Approximately equal to
<= | Lexicographically less than or equal to
>= | Lexicographically greater than or equal to
& | AND
| | OR
! | NOT

The ADSI search filters are used in two ways. They form a part of the LDAP dialect for submitting queries through the OLE DB provider. They are also used with the IDirectorySearch interface.

Dialect

A dialect, as defined in ADSI, refers to the syntax used in a query statement. ADSI supports the SQL and LDAP dialects.

SQL Dialect

Derived from the Structured Query Language, the SQL dialect uses more human-readable expressions to define query statements. SQL statements have the following syntax:

SELECT [ALL] select-list FROM 'ADsPath' [WHERE search-condition] [ORDER BY sort-list]

Where SELECT (may be followed by ALL) and FROM are the required keywords and WHERE and ORDER BY are the optional keywords.

The following are examples of query statements of the SQL dialect in ADSI.

SELECT ADsPath, cn FROM 'LDAP://DC=ArcadiayBay,DC=COM' WHERE objectCategory='group'
Search for all the group objects.
SELECT ADsPath, cn FROM 'LDAP://OU=Sales, DC=ArcadiaBay,DC=COM' WHERE objectCategory='person' AND objectClass='user' AND sn = 'H*' ORDER BY sn
 Search for all the users whose Last Name starts with letter H.

LDAP Dialect

An LDAP dialect string consists of four parts separated by semicolons (;).

  • Base Distinguished Name. For example:
    <LDAP://DC=ArcadiaBay,DC=COM>
  • LDAP Search Filters: For example:
    (&(objectCategory=person)(objectClass=contact)(sn=jsmith))
  • Attributes. Such as ADsPath, department, and manager.
  • Search Scope. Such as base, one-level, or subtree.

The following is an example of the LDAP dialect in ADSI.

"<LDAP://DC=ArcadiaBay,DC=com>;(objectClass=*);AdsPath, cn; subTree"
 Searches all the objects in the subtree.

Not all search options (for example, search scope, search page size) can be expressed in the SQL dialect, so you need to set the options before the actual query execution starts.


Interfaces

There are three types of interfaces in ADSI. The application environment and other requirements may dictate which interface you use.

  • IDirectorySearch. IDirectorySearch is a fairly simple COM interface available to C/C++ programmers only.
  • ADO. The ActiveX Data Object interfaces are OLE Automation interfaces that utilize OLE DB. ADO should be used for queries within applications that rely on OLE Automation. This includes Visual Basic applications, Active Server Pages, and so on.
  • OLE DB. OLE DB is a set of C/C++ interfaces for querying databases. By supporting these interfaces, ADSI providers can participate in advanced OLE DB functionality, such as distributed queries with other OLE DB providers.

All interfaces allow setting the following search preferences:

  • Asynchronous operation
  • Time limit for a search
  • Whether the server should return the names of attributes that are empty
  • Base, One Level, or Sub-Tree depth to the search
  • Size limit for a result set
  • Time limit for the server to process a result set
  • Page size for the result set
  • Timeout for generating a page in a result set
  • Whether or not to chase referrals
  • Whether or not to sort the result set


IDirectorySearch Interface

The IDirectorySearch interface provides a very high level and low overhead interface for querying data of a directory or a global catalog. The IDirectorySearch COM interface can only be used with a vtable, and thus, is not available to Automation-based development environments (such as Visual Basic, VBScript, and so on).

To perform a search

  1. Bind to an object in the directory.
  2. Call QueryInterface to get the IDirectorySearch pointer.
  3. Execute the search using the IDirectorySearch pointer. Call the IDirectorySearch::ExecuteSearch method, and pass a search filter, the requested attribute names, and other parameters.

Execution of a query is provider specific. In some providers, the actual query execution does not take place until IDirectorySearch::GetFirstRow or IDirectorySearch::GetNextRow is called. The IDirectorySearch interface works with search filters directly. Neither the SQL dialect nor the LDAP dialect are required.

The IDirectorySearch interface provides methods to enumerate the result set, row by row. The IDirectorySearch::GetFirstRow method retrieves the first row and IDirectorySearch::GetNextRow moves you to the next row from the current row. When you have reached the last row, calling these methods returns the S_ADS_NOMORE_ROWS error code. On the other hand, IDirectorySearch::GetPreviousRow moves you back one row at a time. And an S_ADS_NOMORE_ROWS return value indicates that you have reached the first row of the result set. These methods operate on the result set resident in memory on the client. Thus, when paged and asynchronous searches are performed and the _CACHE_RESULTS option is turned off, backward scrolling could have unexpected consequences.

Once you have located the appropriate row, you can call IDirectorySearch::GetColumn to obtain data items, column by column. For each call, you pass in the name of the column of interest. The data item returned is a pointer to an ADS_SEARCH_COLUMN structure. GetColumn allocates this structure for you, but you must free it, using FreeColumn. You must also call CloseSearchHandle to finish the search operation.

SetSearchPreference
 Sets options for conducting a search.
ExecuteSearch
 Executes an individual search.
AbandonSearch
 Abandons search already under way.
GetFirstRow
 Gets the first row of the search result.
GetNextRow
 Gets the next row of the search result.
GetPreviousRow
 Gets the previous row of the search result.
GetNextColumnName
 Gets the name of the next column of the search result.
GetColumn
 Gets the item in a specified column from the current row of the search result.
FreeColumn
 Frees the ADS_SEARCH_COLUMN structure created by GetColumn.
CloseSearchHandle
 Releases the search result from memory.

To perform a directory search

  1. Bind to an LDAP provider (may be a domain controller or a global catalog provider).
  2. Retrieve the IDirectorySearch COM Interface. This may be done with a call to QueryInterface, or it may have been done in Step 1 during the initial binding.

    Optionally, call SetSearchPreference to select options for handling the results of your search.

  3. Call ExecuteSearch. Depending on the options set in SetSearchPreference this may or may not begin execution of the query.
  4. Call GetNextRow to move the row index (internal to IDirectorySearch) to the first row.
  5. Read the data from the row using GetColumn, then call FreeColumn to release the memory allocated by GetColumn.
  6. Repeat Step 5 until finished retrieving data from the search result OR until GetNextRow returns S_ADS_NOMORE_ROWS
  7. When done, call AbandonSearch and CloseSearchHandle.

As an example of the scenario above, start out binding to ADSI by calling ADsOpenObject:

HRESULT hr; // COM result variable
ADS_SEARCH_COLUMN col;  // COL for iterations

// Interface Pointers
IDirectorySearch     *pDSSearch    =NULL;
IADs        *pObj    = NULL;
IADs        *pIADs    = NULL;

// Initialize COM
CoInitialize(0);

// Open a connection with server
hr = ADsGetObject(L"LDAP://coho.salmon.arcadiabay.com",
IID_IDirectorySearch,
(void **)&pDSSearch);

This provides a pointer to the IDirectorySearch interface.

Now that there's a COM interface for an IDirectoryInterface instance, you may wish to call IDirectorySearch::SetSearchPreference. The way to do this is covered in detail in the next section.

Next, you will need to build an array of attribute names to prepare to call the IDirectorySearch::ExecuteSearch function . The attribute names are defined within the schema of Active Directory. Refer to the ADSI section of the Platform SDK documentation to see the schema definition. The attribute names listed are returned (if supported by the object) as the result set of the search.

LPWSTR pszAttr[] = { L"description", L"Name", L"distinguishedname" };
ADS_SEARCH_HANDLE hSearch;
DWORD dwCount = 0;
DWORD dwAttrNameSize = sizeof(pszAttr)/sizeof(LPWSTR);

Now, call the ExecuteSearch function. The search does not actually take place, however, until you call the GetNextRow method.

// Search for all objects with the 'cn' property beginning with c
hr = pDSSearch->ExecuteSearch(L"(cn=c*)",pszAttr ,dwAttrNameSize,&hSearch );

Now call GetNextRow to iterate through the rows in the result. Each row is then queried for the "description" attribute. If the attribute is found, it is displayed.

LPWSTR pszColumn;
    while( pDSSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
    {
        // Get the property
        hr = pDSSearch->GetColumn( hSearch, L"description", &col );

        // if this object SUPPORTS this attribute, display it
        if ( SUCCEEDED(hr) )
        {
wprintf(L"The description property:%s\r\n",col.pADsValues->CaseIgnoreString);
            pDSSearch->FreeColumn( &col );
        }
        else
            puts("description property NOT available");
        puts("------------------------------------------------");
        dwCount++;
    }
    pDSSearch->CloseSearchHandle(hSearch);

You can call the AbandonSearch method to end the search at any time.

Note If a page size is NOT set, GetNextRow will block until the entire result set is returned to the client. If page size is set, then GetNextRow will block until the first page (page size = number of rows in a page) is returned. If page size is set and the query is to be sorted and you are NOT searching on at least one indexed attribute, then the page size value is ignored, and the server calculates the entire result set prior to returning the data. This has the effect of GetNextRow blocking until the query completes.

To change this query from a directory search to a global catalog search, the AdsOpenObject call is changed:

    // Open a connection with server
    hr = ADsOpenObject(L"GC://coho.salmon.arcadiabay.com",
    L"Administrator",
    L"",
    ADS_SECURE_AUTHENTICATION,
    IID_IDirectorySearch,
    (void **)&pDSSearch);

Searching Binary Data

In ADSI you can search for binary data, even though ADSI works with strings, which cannot represent binary data. You can get around this limitation by converting the binary data to a string and making queries against the encoded string. ADSI exposes the ADsEncodeBinaryData function for converting binary data to a string. The following skeleton code illustrates how you might proceed to search for binary data.

BYTE *pFingerPrint;
DWORD dwSize;
/* … you get the FingerPrint data and size… */
hr = ADsEncodeBinaryData ( pFingerPrint, dwSize, &pszDest );

/* … do a search on the converted string, pszDest */

FreeADsMem( pszDest );

You must also free the string by calling the FreeADsMem function.

IDirectorySearch::SetSearchPreference

Calling the IDirectorySearch::SetSearchPreference method changes the way in which the search results are obtained and presented through the IDirectorySearch interface.

The Platform SDK documentation defines SetSearchPreference as follows:

HRESULT SetSearchPreference(
            //Search preferences to be set
            PADS_SEARCHPREF_INFO pSearchPrefs,
            //Number of preferences
            DWORD dwNumPrefs
            );

Multiple preferences may be set by passing an array as the first parameter and the array size as the second parameter.

PADS_SEARCHPREF_INFO arSearchPrefs[2];

arSearchPrefs [0].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
arSearchPrefs [0].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [0].vValue.Integer = 100;

arSearchPrefs [1].dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
arSearchPrefs [1].vValue.dwType = ADSTYPE_INTEGER;
arSearchPrefs [1].vValue.Integer = ADS_SCOPE_SUBTREE;

hr = pDSearch->SetSearchPreference(arSearchPrefs, 2);

This example sets the page size to 100 rows and the scope to the ADS_SCOPE_SUBTREE type. The page size setting causes the server to immediately return data to the client, after 100 rows have been calculated. The ADS_SCOPE_SUBTREE setting causes the search to encompass all branches in the tree below the point from which the search is being executed.


ADO Access with Visual Basic

In order for ADO to communicate with ADSI, you need to have at least three ADO objects: a Connection, a Command, and a RecordSet. These ADO objects serve to authenticate users, initiate queries, and enumerate results, respectively. The Connection object loads the OLE DB provider, and validates user credentials. In Visual Basic, you call CreateObject("ADODB.Connection") to create an instance of a Connection object, and then set the Provider property of the Connection object to "ADsDSOObject". "ADODB.Connection" is the ProgID of the Connection object and "ADsDSOObject" is the name of the OLE DB provider in ADSI. If no credentials are specified, the credentials of the currently logged on user are assumed.

The following code snippet illustrates how to create an instance of a Connection object using VBScript.

Set con = CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"

The next example is an equivalent Active Server Page (ASP) code snippet.

<%
Set con = Server.CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"
%>

Here is the same example in Visual Basic. Note that you must include the ADO type library (msadoXX.dll) as one of the references in the VB project.

Dim Con As New Connection
con.Provider = "ADsDSOObject"

The second ADO object is the Command object. The ProgID is "ADODB.Command". This object lets you issue query statements and other commands to ADSI using the active connection. The Command object uses its ActiveConnection property to maintain an active connection. It also maintains the CommandText property to hold query statements issued by a user. The query statements are expressed in either the SQL dialect or the LDAP dialect. The following code snippets illustrate how to create a Command object.

VBScript example:

Set command = CreateObject("ADODB.Command")
Set command.ActiveConnection = con
command.CommandText =
"SELECT AdsPath, cn, FROM '<LDAP://DC=ArcadiaBay,DC=com>'
 WHERE objectClass = '*'"

VB example: Note you must include ADO type library (msadoXX.dll) as one of the references.

Dim command As New Command
Set command.ActiveConnection = con
command.CommandText =
    "<LDAP://DC=ArcadiaBay,DC=com>;(objectClass=*);AdsPath, cn; subTree"

You can specify search options by setting the property of the Command object actually called Property. Acceptable properties are listed in the following table.

SearchScope | Enumeration | ADS_SCOPE_SUBTREE
Timeout | Integer | No time out
Page size | Integer | No page size
Time limit | Integer | No time limit
Chase Referrals | Enumeration | ADS_CHASE_REFERRALS_EXTERNAL
Cache Results | Boolean | True

The following code example illustrates how to set search options in Visual Basic.

Dim Com As New Command

Com.Properties("Page Size") = 1000
Com.Properties("Timeout") = 30     'seconds
Com.Properties("searchscope") = ADS_SCOPE_ONELEVEL     'Define in ADS_SCOPEENUM
Com.Properties("Chase referrals") = ADS_CHASE_REFERRALS_EXTERNAL
Com.Properties("Cache Results") = False     'do not cache the result set

The third ADO object is RecordSet. You obtain this object when you invoke the Execute method on a Command object. The primary function of the RecordSet object is to enumerate the result set and obtain the data. The result set can contain both single- or multiple-valued attribute values. Getting a single-valued attribute is straightforward, similar to getting the column value in the relational database (for example, Fields('name').Value). Getting a multi-valued attribute, however, is more challenging. In this case, the Field.Value is an array and you must check the lower and upper bound of the array, as illustrated in the following example.

Set rs = Com.Execute

For i = 0 To rs.Fields.Count - 1
  Debug.Print rs.Fields(i).Name, rs.Fields(i).Type
Next i

'--------------------------------------
' Navigate the record set
'----------------------------------------
rs.MoveFirst
lstResult.Clear      'Clear the user interface
While Not rs.EOF
For i = 0 To rs.Fields.Count - 1
    'For Multi Value attribute
    If rs.Fields(i).Type = adVariant And Not (IsNull(rs.Fields(i).Value)) Then
        Debug.Print rs.Fields(i).Name, " = "
        For j = LBound(rs.Fields(i).Value) To UBound(rs.Fields(i).Value)
            Debug.Print rs.Fields(i).Value(j), " # "
            lstResult.AddItem rs.Fields(i).Value(j)
        Next j
    Else
        'For Single Value attribute
         Debug.Print rs.Fields(i).Name, " = ", rs.Fields(i).Value
         lstResult.AddItem rs.Fields(i).Value
    End If
Next i
rs.MoveNext
Wend

Modifying an ADSI Object from ADO

ADSI 2.5 ships with a read-only OLE DB provider. This means that you cannot issue the UPDATE statement in the SQL dialect at present. In the future, ADSI will support a readable and writeable OLE DB provider.

To modify an object returned from an ADO query

  1. Ask for the ADsPath when specifying attribute names, as in "SELECT ADsPath, …"
  2. Execute the query and get the ADsPath attribute.
  3. Bind to the record set using ADsPath, and modify the attributes.

The following code example illustrates how to modify an ADSI object after performing the steps outlined in the preceding example.

'Replacing department for all users in OU=sales
Set con = Server.CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"

Set command = CreateObject("ADODB.Command")
Set command.ActiveConnection = con

command.CommandText =
"SELECT AdsPath, cn, FROM '<LDAP://OU=Sales,DC=ArcadiaBay,DC=com>'
 WHERE objectClass = 'user'"

Com.Properties("searchscope") = ADS_SCOPE_ONELEVEL
Set rs = Com.Execute
While Not rs.EOF
    Set usr = GetObject(rs.Fields('AdsPath').Value)
    usr.Put "department", "1001"
    usr.SetInfo
    rs.MoveNext
Wend


OLE DB Access with C/C++ and COM

Active Directory ships with an OLE DB provider. This allows the developer to access Active Directory using either OLE DB directly or the ActiveX Data Objects (ADO) layer.

To perform an Active Directory query using OLE DB

  1. Retrieve the IDBInitialize interface from the OLE DB provider. In the case of Active Directory, use the class id "CLSID_ADsDSOObject".
  2. Build a DBPROP array of connection information specifying user name and password.
  3. Query the IDBInitialize interface retrieved in Step 1 for the IDBProperties interface.
  4. Call the IDBProperties::Setproperties method passing in the DBPROP array created in Step 2.
  5. Call the IDBInitialize::Initialize method to establish the connection to the OLE DB provider (the Active Directory provider, in this case).
  6. Query the IDBInitialize interface for the IDBCreateSession interface.
  7. Call the IDBCreateSession::CreateSession method requesting a new interface of type IDBCreateCommand.
  8. Call the IDBCreateCommand::CreateCommand method to create an ICommandText interface.
  9. Call the ICommandText::SetCommandText method. Pass in the preferred dialect and the actual query command text in that dialect. Either DBGUID_LDAPDialect or DBGUID_DBSQL may be used as the dialect.
  10. Call ICommandText::Execute, an IRowset interface is returned which is the interface to the result set.
  11. Query the IRowset interface for the IColumnsInfo interface.
  12. Call the IColumnsInfo::GetColumnInfo method to retrieve column information about the result set.
  13. Populate an array of DBBINDING structures, describing to the OLE DB provider how to expose the data types on a per column basis to our application code. This step allows you to specify which TYPE is contained in a particular column. Also the offsets of the columns (relative to the returned row) are set here on a column by column basis.
  14. Query the IRowset interface for the IAccessor interface.
  15. Call the IAccessor::CreateAccessor method which returns an array of accessor handles. This array is then used to access the rows of the result set.
  16. Call IRowset::GetNextRows passing in the row handles, and number of rows to fetch.
  17. Call IRowset::GetData passing in a row handle (from the set returned in Step 16). A raw pointer to the row is returned.

To read the raw row data returned, use the DBBINDING structure (created in Step 13), compute the column offsets in the raw data pointer returned in Step 17. Read the status portion of the column for a retrieval result on that column.


OLE DB and Active Directory Access Code Walkthrough

The following sample code illustrates how to call Active Directory using COM, C++, and OLE DB. This sample is a console application which searches the directory for all users under the coho.salmon.arcadiabay.com tree and displays the name and ADsPath properties to the console window.

ADSI defines two GUIDs for dialects:

  • DBGUID_SQL for the SQL dialect.
  • DBGUID_LDAPDialect for the LDAP dialect.

The following sample uses the LDAP dialect.

You may also pass DBGUID_DEFAULT as the dialect. In this case, ADSI will try to use the SQL dialect first, if that fails, it will retry using the LDAP dialect.

For more information about OLE DB, refer to the OLE DB programming manual.

Variable Definitions

The following are COM Interface pointers for OLE DB:

IDBInitialize*       pIDBInitialize = NULL;
IDBCreateSession*    pCreateSession = NULL;
IDBCreateCommand*    pICreateCommand = NULL;
IRowset*             pIRowset = NULL;
ICommandText*        pICommandText = NULL;
IDBProperties*       pIDBProperties = NULL;
IColumnsInfo*        pIColumnsInfo = NULL;
IAccessor*           pIAccessor = NULL;

HACCESSOR            hAccessor = NULL;     //Accessor Handle
DBCOLUMNINFO*        pDBColumnInfo;        //Column info for returned rows
DBPROP               InitProperties[4];    //Array for OLE DB connection info
DBPROPSET            rgInitPropSet[1];     //Array for OLE DB connection info
int                  i;
ULONG                cCol;
ULONG                cbColOffset = 0;
DBBINDING            rgBind[MAX_COL];      //Array for Column Binding info
HROW                 hRows[5];             //Array of Handles of returned rows
HROW*                pRows = &hRows[0];    //Pointer to rows
LONG                 cNumRows;             //Num rows returned
WCHAR*               pStringsBuffer;       //String Buffer
HRESULT              hr;                   //COM HRESULT variable
ULONG                cMaxRowSize;          //buffer size for 1 row's data

Query String

In the following example, set up a query string using the LDAP dialect:

LPCTSTR wCmdString =OLESTR( "<LDAP://coho.salmon.arcadiabay.com/cn=users,dc=coho,dc=salmon,dc=arcadiabay,dc=com>;((objectClass=user)); name, adspath;subtree"
);

// Initialize The Component Object Module library
CoInitialize(NULL);

ADsDSOObject

Now the COM function CoCreateInstance is called. The class CLSID_ADsDSOObject refers to the OLE DB Active Directory provider. This function will return an IDBInitialize interface. This interface is the root of our OLE DB connection.

// Obtain access to the OLE DB - ODBC provider - CLSID_ADsDSOObject
hr =CoCreateInstance(CLSID_ADsDSOObject , NULL, CLSCTX_INPROC_SERVER,
            IID_IDBInitialize, (void **) pIDBInitialize);

DBPROP Array/OLE DB Connection and Initialization

In preparation for calling the IDBProperties::SetProperties method, build an array of the properties for the OLE DB connection. The following code initializes the DBPROP INITPROPERTIES array, and fills in the user ID and password, and turns off UI prompting (DBPROP_INIT_PROMPT).

for (i = 0; i < 3; i++ )
{
    VariantInit(&InitProperties[i].vValue);
    InitProperties[i].dwOptions = DBPROPOPTIONS_REQUIRED;
    InitProperties[i].colid = DB_NULLID;
}

// level of prompting that will be done to complete the connection process
InitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
InitProperties[0].vValue.vt = VT_I2;
InitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;

Fill in the User Name

InitProperties[1].dwPropertyID = DBPROP_AUTH_USERID;
InitProperties[1].vValue.vt = VT_BSTR;
InitProperties[1].vValue.bstrVal = SysAllocString((LPOLESTR)L"user");

Fill in the Password

InitProperties[2].dwPropertyID = DBPROP_AUTH_PASSWORD;
InitProperties[2].vValue.vt = VT_BSTR;
InitProperties[2].vValue.bstrVal = SysAllocString((LPOLESTR)L"password");

Set Initialization Properties

Now, call QueryInterface to get the IDBInitialize interface for the IDBProperties interface. Call the IDBProperties::SetProperties method to pass the DBPROPSET structure, which contains a pointer to the InitProperties DBPROP structure that was filled in a previous step.

// Set our InitProperties array into the property set
rgInitPropSet[0].guidPropertySet = DBPROPSET_DBINIT;
rgInitPropSet[0].cProperties = 4;
rgInitPropSet[0].rgProperties = InitProperties;

// set initialization properties
pIDBInitialize->QueryInterface(IID_IDBProperties, (void **)&pIDBProperties);
pIDBProperties->SetProperties(1,rgInitPropSet);
pIDBProperties->Release();

Perform the Connection

Now, call the Initialize method to establish the connection This step actually hooks up IDBInitialize with the directory.

pIDBInitialize->Initialize();

Create a Session Object from the IDBInitialize Interface

hr = pIDBInitialize->QueryInterface(IID_IDBCreateSession, (void **) &pCreateSession);

Create an IDBCreateCommand Interface from the IDBCreateSession Interface

hr = pCreateSession->CreateSession(NULL, IID_IDBCreateCommand, (IUnknown **) &pICreateCommand);

Create an ICommandText Interface from the ICreateCommand Interface

hr = pICreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown **) &pICommandText);

Set the Command Text

Before executing the query, pass the command text for the query along with the dialect of the command text. In this case, use the LDAP dialect. You may also pass DBGUID_DBSQL, providing the query text is in that format.

hr = pICommandText->SetCommandText(DBGUID_LDAPDialect, wCmdString);

Do the Query

Next, tell Active Directory OLE DB provider to execute the statement. If successful, an IRowset interface is returned.

hr= pICommandText->Execute(NULL,
    IID_IRowset,                 // Interface requested
    NULL,
    &cNumRows,                   // Number of rows returned
    (IUnknown **) &pIRowset);    // IRowset interface

Retrieve Column Information

An IRowset object is now available. Send a query for the IColumnsInfo interface to obtain the column information of the result set.

hr = pIRowset->QueryInterface(IID_IColumnsInfo, (void **) &pIColumnsInfo);
CheckHRESULT(hr,"");

Now, use the IColumnsInfo::GetColumnsInfo method to get the column information from the command object.

hr = pIColumnsInfo->GetColumnInfo( &cCol, &pDBColumnInfo, &pStringsBuffer );

CheckHRESULT(hr,"");
DWORD    dwText;

Display the name of each column in the result set. Delimit the output with tab characters.

wszDispBuffer[0]='\0';
for(ULONG nCount=0 ; nCount < cCol; nCount++)
{
    if (pDBColumnInfo[nCount].pwszName)
        wcscat(wszDispBuffer, pDBColumnInfo[nCount].pwszName);
    dwText = wcslen(wszDispBuffer);
    wszDispBuffer[dwText++] = L'\t';
    wszDispBuffer[dwText] = L'\0';
}

// NULL Terminate the Display Buffer
if (*wszDispBuffer)
    wszDispBuffer[wcslen(wszDispBuffer)-1]=L'\0';

_putws(wszDispBuffer);

Setup Column Bindings

Next, create column bindings for the result set. This code essentially tells the IAccessor (created in the following example) to return all column data as Unicode strings. This simplifies this example as no type coercion is necessary.

DWORD dwOffset = 0;
for (ULONG ulBind=0; ulBind < cCol; ulBind++)
{
    // Binding Structure
    rgBind[ulBind].dwPart      = DBPART_VALUE | DBPART_LENGTH |
                                  DBPART_STATUS;
    rgBind[ulBind].eParamIO    = DBPARAMIO_NOTPARAM;
    rgBind[ulBind].iOrdinal    = pDBColumnInfo[ulBind].iOrdinal;
    rgBind[ulBind].wType       = DBTYPE_WSTR;
    rgBind[ulBind].pTypeInfo   = NULL;
    rgBind[ulBind].obValue     = dwOffset + offsetof(DBCOLUMNDATA,bData);
    rgBind[ulBind].obLength    = dwOffset + offsetof(DBCOLUMNDATA,dwLength);
    rgBind[ulBind].obStatus    = dwOffset + offsetof(DBCOLUMNDATA,wStatus);
    rgBind[ulBind].cbMaxLen    = MAXROWLEN;
    rgBind[ulBind].pObject     = NULL;
    rgBind[ulBind].pBindExt    = NULL;
    rgBind[ulBind].dwFlags     = 0;
    rgBind[ulBind].dwMemOwner  = DBMEMOWNER_CLIENTOWNED;
    rgBind[ulBind].bPrecision  = 0;
    rgBind[ulBind].bScale      = 0;

    dwOffset += rgBind[ulBind].cbMaxLen + offsetof( DBCOLUMNDATA, bData );
    dwOffset = ROUND_UP( dwOffset, COLUMN_ALIGNVAL );
    //iBind++;
}
cMaxRowSize = dwOffset;

Create the Accessor

Use the IAccessor interface to get an accessor for our bindings from the rowset. Then call the IAccessor::CreateAccessor method, which returns an array of accessor handles. This is an array of handles to the rows in the result set.

hr = pIRowset->QueryInterface( IID_IAccessor, (void**)&pIAccessor );

CheckHRESULT(hr,"");

hr = pIAccessor->CreateAccessor(
                    DBACCESSOR_ROWDATA,
                    ulBind,
                    rgBind,
                    0,
                    &hAccessor,
                    NULL );

Retrieve the Data One Row At a Time

First, allocate a block of memory (pRowData) which will hold one row of the result set. Then call the IRowset::GetNextRows method to pass in the number of rows desired, and pointers to the return row data (pRowData), and the number of rows returned (cRowsObtained). When the row data is available, call the PrintRowData function.

BYTE*    pRowData = NULL;            // Memory for Data

// Create a buffer for row data, big enough to hold the biggest row
pRowData = (BYTE *) malloc( cMaxRowSize );

ULONG     cRowsObtained;            // Number of rows obtained
HROW     rghRows[NUMROWS_CHUNK];    // Row Handles

// Process all the rows, one by one, NUMROWS_CHUNK rows at a time
do
{
    hr = pIRowset->GetNextRows(
        0,                         // cbChapter
        0,                         // cRowsToSkip
        NUMROWS_CHUNK,             // cRowsDesired
        &cRowsObtained,            // cRowsObtained
        &pRows );                  // filled in w/ row handles

    // If you are NOT done retrieving rows
    if ( cRowsObtained )
    {
        // Loop over rows obtained, getting data for each
        for (ULONG ulRow=0; ulRow < cRowsObtained; ulRow++ )
        {
            // Actually retrieves the row
hr = pIRowset->GetData(
                pRows[ulRow],      // Row Handle
                hAccessor,         // Handle to Accessor
                pRowData );        // Pointer to Row Data

            // Print to the Screen
            PrintRowData( rgBind,pDBColumnInfo, ulBind, pRowData );
        }
        // Release row handles
        hr = pIRowset->ReleaseRows( cRowsObtained, rghRows, 0, 0, 0);
    }
} while (cRowsObtained);

PrintRowData Function

This function takes the column binding structure (DBBINDING), the column information structure (DBCOLUMNINFO), the number of columns bound, and a BYTE pointer to the row data. It retrieves the column offsets in the row data, retrieves the column data for that column, and displays the data to the console window in tab delimited format. The dwStatus attribute of the DBCOLUMN structure is read to assert that the OLE DB provider was able to successfully retrieve the column data. Recall that when you initially bound the columns, it was specified that all column data shall come back as Unicode strings.

void PrintRowData
(
DBBINDING*     rgBind,           // DBBINDING Structure
DBCOLUMNINFO*  pDBColumnInfo,    // DBCOLUMNINFO Structure
ULONG          ulNumColsBind,    // Number of columns that are bound to
BYTE*          pData             // Row data retrieved with
// IRowset::GetData()
 )

{
    DWORD      dwOutIndex =0;    // Index for tracking output String
    ULONG      ulCurrentCol;     // Index for the current column
    DBCOLUMNDATA*    pColumn;    // Data Structure

    // Print each column that is bound to.
    for (ulCurrentCol=0, wszDispBuffer[0]='\0';
ulCurrentCol < ulNumColsBind; ulCurrentCol++)
    {
        // Grab a pointer to the column info -
// found right behind the status offset
        // for this column in the data buffer
        pColumn = (DBCOLUMNDATA *) (pData + rgBind[ulCurrentCol].obStatus);

        if (pDBColumnInfo[ulCurrentCol].pwszName)
        {
            switch (pColumn->wStatus)
            {
                case DBSTATUS_S_ISNULL:
                    wcscat(wszDispBuffer, L"<NULL>");
                    break;
                case DBSTATUS_S_OK:
                case DBSTATUS_S_TRUNCATED:
                {
                    if ((LPWSTR)pColumn->bData)
                        wcscat (wszDispBuffer,
(LPWSTR)pColumn->bData);
                    else
                        wcscat (wszDispBuffer,L"null");
                    break;
                }
                case DBSTATUS_E_CANTCONVERTVALUE:
wcscat(wszDispBuffer, L"<can't convert value>");
                    break;
                default:
                    wcscat(wszDispBuffer,L"<unknown>");
                    break;
            }

            dwOutIndex = wcslen(wszDispBuffer);
            wszDispBuffer[dwOutIndex++] = L'\t';
            wszDispBuffer[dwOutIndex] = L'\0';
        }
    }
    // The last tab goes away
    wszDispBuffer[--dwOutIndex] = L'\0';

    // Print the row to the screen
_putws(wszDispBuffer);

}


Sample Code Output

Following is the output from the previous sample code. This result set lists all the users in the coho.salmon.arcadiabay.com tree. The LDAP dialect query text used to obtain these results is:

"<LDAP://coho.salmon.arcadiabay.com/cn=users,dc=coho,dc=salmon,dc=arcadiabay,dc=com>; (&(objectCategory=person)(objectClass=user); name, adspath; subtree"

Guest

LDAP://coho.salmon.arcadiabay.com/CN=Guest,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

MTS_T-MACHINEMACHINE

LDAP://coho.salmon.arcadiabay.com/CN=MTS_T-MACHINE,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

IWAM_T-MACHINEMACHINE

LDAP://coho.salmon.arcadiabay.com/CN=IWAM_T-MACHINE,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

SQLExecutiveCmdExec

LDAP://coho.salmon.arcadiabay.com/CN=SQLExecutiveCmdExec,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

SQLAgentCmdExec

LDAP://coho.salmon.arcadiabay.com/CN=SQLAgentCmdExec,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

IUSR_T-MACHINE

LDAP://coho.salmon.arcadiabay.com/CN=IUSR_T-MACHINE,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Administrator

LDAP://coho.salmon.arcadiabay.com/CN=Administrator,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

James Smith

LDAP://coho.salmon.arcadiabay.com/CN=James Smith,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Neil Smith

LDAP://coho.salmon.arcadiabay.com/CN=Neil Smith,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Suzan Fine

LDAP://coho.salmon.arcadiabay.com/CN=Suzan Fine,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Jenny Sax

LDAP://coho.salmon.arcadiabay.com/CN=Jenny Sax,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Amy Egert

LDAP://coho.salmon.arcadiabay.com/CN=Amy Egert,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Suki White

LDAP://coho.salmon.arcadiabay.com/CN=Suki White,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Scott Cooper

LDAP://coho.salmon.arcadiabay.com/CN=Scott Cooper,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Meng Phua

LDAP://coho.salmon.arcadiabay.com/CN=Meng Phua,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Lani Ota Carpenter

LDAP://coho.salmon.arcadiabay.com/CN=Lani Ota Carpenter,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Tristan Randall

LDAP://coho.salmon.arcadiabay.com/CN=Tristan Randall,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com

Tom Johnston

LDAP://coho.salmon.arcadiabay.com/CN=Tom Johnston,CN=Users,DC=coho,DC=salmon,DC=arcadiabay,DC=com


Distributed Query

Since ADSI is an OLE DB provider, it can participate in Distributed Query introduced in Microsoft® SQL 7. This brings up many interesting scenarios:

  • Joining Active Directory objects with SQL Server data.
  • Updating SQL data from Active Directory objects.
  • Three- or four-way joins with other OLE DB providers. For example, Index Server, SQL Server, and Active Directory.

To set up a distributed join, you must first set up ADSI mapping using the sp_addlinkedserver stored procedure in a SQL Server Tool such as SQL Query Analyzer. This procedure links a name to an OLE DB provider's name.

For example:

sp_addlinkedserver 'ADSI', 'Active Directory Services 2.0', 'ADSDSOObject', 'adsdatasource'

Optionally, you can use sp_addlinkedsrvlogin to specify which user should be run. Since SQL 7 participates in Single Sign On (SSO), Kerberos delegation can be used when Kerberos is the chosen authentication package. If you elect to use Kerberos, you should not run this option.

Example of an sp_addlinkedsrvlogin call:

sp_addlinkedsrvlogin ADSI, false, 'REDMOND\Administrator', 'CN=Administrator,OU=Sales, DC=actived,DC=arcadiabay,DC=com', 'secret**2000'

Once you have established this link, you can start running the query against the directory.

For example:

select  Name, sn lastName, street Street, l City, st State
from OpenQuery( ADSI,
'SELECT Name,sn, street, l, st, description FROM "LDAP://OU=Sales, DC=actived,DC=Arcadiabay,DC=Com" WHERE objectCategory="person" AND objectClass="contact"')


Searching with ActiveX Data Objects (ADO)

The following example shows how Automation clients can use ActiveX Data Objects (ADO) to query directory services using ADSI and its OLE DB implementation. The ADO object model consists of the following objects:

Connection
An open connection to an OLE DB data source such as ADSI.
Command
Defines a specific command to execute against the data source.
Parameters
An optional collection for any parameters to provide to the command object.
Recordset
A set of records from a table, command object, or SQL syntax. A recordset can be created without any underlying Connection object.
Field
A single column of data in a recordset.
Property
A collection of values supplied by the provider for ADO.
Error
Contains details about data access errors, refreshed for each time an error occurs in a single operation.

For more information, see the Microsoft Platform SDK under ActiveX Data Objects.

The following example disables the user accounts on an LDAP server.

Dim X as IADs
Dim con As New Connection, rs As New Recordset
Dim MyUser As IADsUser

con.Provider = "ADsDSOObject"
con.Open "Active Directory Provider", "CN=Foobar,CN=Users,DC=ARCADIABAY,DC=COM,O=INTERNET", "Password"
Set rs = con.Execute("<LDAP://MyMachine/DC=MyDomain,DC=ArcadiaBay,DC=com>;(objectClass=User);ADsPath;onelevel")

While Not rs.EOF
    ' Bind to the object to make changes
    ' to it since ADO is currently read-only
    MyUser = GetObject(rs.Fields(0).Value)
    MyUser.AccountDisabled = True
    rs.MoveNext
Wend


LDAP Dialect

Wildcards and conditions can be added to an LDAP string to search the directory. The following examples show substrings that can be used to search the directory.

Get all entries:

(objectClass=*)

Get entries containing 'bob' somewhere in the common name:

(cn=*bob*)

Get entries with a common name greater than or equal to 'bob':

(cn>='bob')

Get all users with an email attribute:

(&(objectClass=user)(email=*))

Get all user entries with an email attribute and a surname equal to 'smith':

(&(sn=smith)(objectClass=user)(email=*))

Get all user entries with a common name that starts with 'andy', 'steve', or 'margaret':

(&(objectClass=user) | (cn=andy*)(cn=steve)(cn=margaret))

Get all entries without an email attribute:

(!(email=*))

The formal definition of the search filter is as follows (from RFC 1960):

<filter> ::= '(' <filtercomp> ')'
<filtercomp> ::= <and> | <or> | <not> | <item>
<and> ::= '&' <filterlist>
<or> ::= '|' <filterlist>
<not> ::= '!' <filter>
<filterlist> ::= <filter> | <filter> <filterlist>
<item> ::= <simple> | <present> | <substring>
<simple> ::= <attr> <filtertype> <value>
<filtertype> ::= <equal> | <approx> | <ge> | <le>
<equal> ::= '='
<approx> ::= '~='
<ge> ::= '>='
<le> ::= '<='
<present> ::= <attr> '=*'
<substring> ::= <attr> '=' <initial> <any> <final>
<initial> ::= NULL | <value>
<any> ::= '*' <starval>
<starval> ::= NULL | <value> '*' <starval>
<final> ::= NULL | <value>

The token <attr> is a string representing an AttributeType. The token <value> is a string representing an AttributeValue whose format is defined by the underlying directory service.

If a <value> must contain one of the characters * or ( or ), the character should be preceded by \ escape character.

Example

Dim con As New Connection, rs As New Recordset
Dim v
Dim Com As New Command

On Error GoTo bailout

'Open a Connection object
con.Provider = "ADsDSOObject"
Debug.Print con.Properties.Count
For j = 0 To con.Properties.Count - 1
    Debug.Print con.Properties(j).Name
Next j

con.Open "Active Directory Provider", "CN=administrator,CN=users,DC=MyDomain,DC=ArcadiaBay,DC=com", ""

' Create a command object on this connection
Set Com.ActiveConnection = con

' set the query string
Com.CommandText = "<LDAP://MyServer/DC=MyDomain,DC=ArcadiaBay,DC=com>;(objectClass=*);ADsPath, objectclass;base"

For j = 0 To Com.Properties.Count - 1
    Debug.Print Com.Properties(j).Name
Next j

' Set the preferences for Search
Com.Properties("Page Size") = 1000
Com.Properties("Timeout") = 30 'seconds

'Execute the query
Set rs = Com.Execute

For i = 0 To rs.Fields.Count - 1
    Debug.Print rs.Fields(i).Name, rs.Fields(i).Type
Next i

rs.MoveLast
Debug.Print "No. of rows = ", rs.RecordCount

' Navigate the record set
rs.MoveFirst
While Not rs.EOF
    For i = 0 To rs.Fields.Count - 1
        If rs.Fields(i).Type = adVariant And Not (IsNull(rs.Fields(i).Value)) Then
            Debug.Print rs.Fields(i).Name, " = "
            For j = LBound(rs.Fields(i).Value) To UBound(rs.Fields(i).Value)
                Debug.Print rs.Fields(i).Value(j), " # "
            Next j
        Else
            Debug.Print rs.Fields(i).Name, " = ", rs.Fields(i).Value
        End If
    Next i
    rs.MoveNext
Wend

rs.MoveLast
Debug.Print "No. of rows = ", rs.RecordCount

Exit Sub

bailout:     Debug.Print "Error", Hex(Err.Number), " :", Error(Err.Number)
         Exit Sub


SQL Dialect

The following are examples of ADSI queries using SQL:

Select ADsPath, cn FROM 'LDAP://O=Internet/DC=COM/DC=ARCADIABAY/DC=ARCADIADEV' where objectClass='group' :  Select all group objects

Select AdsPath, cn, revision FROM
'LDAP://nw01t1/O=Internet/DC=COM/DC=ARCADIABAY/DC=ARCADIADEV/dc=nwparent' where objectClass='*' AND revision >= 10 : All user objects which have the property revision greater than or equal to 10

Select AdsPath, cn, revision FROM 'LDAP://nw01t1/O=Internet/DC=COM/DC=ARCADIABAY/DC=ARCADIADEV/dc=nwparent' where objectClass='*' AND (cn = 'a*' OR cn = 'b*'):  All Computer objects which have name attribute starting with "a" or "b"

The formal grammar for the SQL queries is defined in the following way. (All keywords are case insensitive.)

statement ::= select-statement
select-statement ::= SELECT [ALL] select-list FROM table-identifier [WHERE search-condition]
select-list ::= * | select-sublist [, select-sublist]...
select-sublist ::= column-identifier
column-identifier ::= user-defined-name
table-identifier ::= string-literal
search-condition ::= boolean-term [OR search-condition]
boolean-term ::= boolean-factor [AND boolean-term]
boolean-factor ::= [NOT] boolean-primary
boolean-primary ::= comparison-predicate | (search-condition)
comparison-predicate ::= column-identifier comparison-operator literal
comparison-operator ::= < | > | <= | >= | = | <>
user-defined-name ::= letter [letter | digit]...
literal ::= string-literal | numeric-literal | boolean-literal
string-literal ::= '{character}...' (Any sequence of characters delimited by quotes)
numeric-literal ::= digits [fraction] [exponent]
digits ::= digit [digit]...
fraction ::= . digits
exponent ::= E digits
boolean-literal ::= TRUE | FALSE | YES | NO | ON | OFF


Searching Properties

These are the connection properties supported:

Property Type Default
"User ID"

BSTR

NULL

"Password"

BSTR

NULL

"Encrypt Password"

BOOLEAN

FALSE

The following Command Properties are supported:

Property | Type | Default
"Asynchronous" | BOOLEAN | FALSE
"Deref Aliases" | BOOLEAN | FALSE
"Size Limit" | Integer | No Limit (0)
"Time Limit" | Integer | None (0)
"Column Names Only" | BOOLEAN | FALSE
"SearchScope" | 0 (Base), 1(onelevel), 2 (subtree) | 2 (subtree)
"Timeout" | Integer | None (0)
"Page Size" | Integer | None (0)
"Time Limit" | Integer | None (0)
"Chase Referrals" | BOOLEAN | FALSE
"Cache Results" | BOOLEAN | TRUE

Example:

Sub DoQuery(stPath, stUser, stPass)

    Set oConnect = CreateObject("ADODB.Connection")
    oConnect.Provider = "ADsDSOObject"
    oConnect.Properties("User ID") = stUser
    oConnect.Properties("Password") = stPass
    oConnect.Properties("Encrypt Password") = True
    oConnect.Open "DS Query", stUser, stPass

    Set oCommand = CreateObject("ADODB.Command")
    Set oCommand.ActiveConnection = oConnect

    PropStr = "aDSPath"
    oCommand.CommandText = "<" & stPath & ">;(objectClass=User);" & PropStr & ";SubTree"
    oCommand.Properties("Page Size") = 50

    Set oRecordSet = oCommand.Execute

    oRecordSet.MoveFirst
    j = 0
    While Not oRecordSet.EOF
        j=j+1
        oRecordSet.MoveNext
    wend
    WScript.Echo "Found " & j & " objects"

End Sub


OLE DB Interface

For both Automation clients using ActiveX Data Objects (ADO) and all non-Automation clients, ADSI supplies an OLE DB provider, supporting a subset of OLE DB query interfaces. Client code that already uses OLE DB interfaces for queries can use the same interfaces to query directory services.

Under the OLE DB implementation, a directory service is exposed as a Data Source Object. Data source objects are factories for session objects and support IDBInitialize to connect to the directory, IDBCreateSession to create the session object, IDBProperties to supply authentication information to the underlying namespace and supply the query command and IPersist to save the information necessary to create the data source object to the underlying directory service. Please see the ADSI OLE DB Example.


ADSI Extensions

This section describes the following topics:

  • Introduction to ADSI Extensions
  • ADSI Extension Architecture
  • Getting ADSI Interfaces From Your Extension
  • ADSI Extension Type Libraries
  • ADSI and Extensions: What's Happening Under the Hood?
  • Late Binding Support
  • IADsExtension Usage
  • Supporting Dual or Dispatch Interfaces
  • Late Binding: What's Happening Under the Hood?
  • Revisiting COM Aggregation Rules with ADSI Extensions
  • What Does a Client See?
  • Resolution of Multiple Aggregation Components Supporting the Same Interface
  • Late Binding vs. Vtable Access in the ADSI Extension Model
  • Resolution of Function/Property Name Conflicts in Automation in Extensions
  • More on the Resolution of Automation Conflicts: Same Function Name but Different Parameters


Introduction to ADSI Extensions

An ADSI extension is a special COM object that allows developers to extend an ADSI object. Each extension is associated with a specified class in the directory. With this extension model, developers can add methods to give more dynamic meaning to an object. In a traditional directory programming model, an object is accessed by getting and setting the object's attributes. Using ADSI extensions, you can add more functionality to a directory object.

The way in which extension methods are implemented is up to the extension writer. An extension writer may even implement a method that is completely outside the scope of the directory. For example, imagine that a developer of backup and restore software is planning to extend an object called computer. The developer will need to create two methods: BackUp and Restore. These methods will operate remotely on the physical computer that the computer object in the directory is pointing to. By participating as an extension, the component automatically gains access to the ADSI infrastructure and is viewed by ADSI clients as an integral part of the object.

Of course, not all components are viable ADSI extensions. The following guidelines should help you decide if creating an ADSI extension is appropriate:

  • Create an extension to integrate your component with the directory object. Since there is a user object in the directory, an HR developer may wish to create an ADSI extension that will populate other information in the directory every time a user is created.
  • Create an extension if your component requires a directory lookup. Your component may require a directory as a starting point for a lookup. For example, say you are creating a new application. Your application object, FooApp, can be published in the directory. Your application may support status notifications on a collection of mail servers. You decide to make this application an ADSI extension.

    Now, a user may search for all instances of FooApp in the directory. For each object returned, the user may issue a call to NotifyNow(), for instance. Your application/extension will be able to get more information about the current object in the directory and notify each server asynchronously.

  • Create an extension as a junction point between namespaces and programming models. For example, an ISV invents a new object model for print services. The printQueue object is already defined in the directory. The ISV can create an ADSI extension and associate it with the printQueue object. ADSI users will be able to bind to a printQueue object and start using the object model for the ISV. From the ADSI client's perspective, this junction point is transparent.
  • Create an extension to simplify tasks. Many tasks in the directory can be accomplished by searching and setting multiple attributes in an object or multiple objects. By creating a single function to manipulate multiple attributes, it simplifies development for application and script writers.

For ADSI clients, extensions enrich the ADSI programming environment in many ways:

  • Developers creating ADSI clients do not have to learn a new programming model. The extensions are part of ADSI. They would use the same paradigm for searching, data manipulation, and securing objects.
  • Administrators can manage related directory-enabled applications using extensions.
  • Extension consumers can view an ADSI object and extension as one integrated object.
  • Existing components may be integrated with ADSI, which allows extensions to leverage existing investments and create a powerful synergy between components.

ADSI extensions were designed with the following goals:

  • Easy to implement. With the current existing Microsoft® technology, Visual C++® and Active Template Library, you can develop an extension in just a few hours.
  • Clients view one IDispatch. From the perspective of script and Automation writers, the extension methods and properties are transparently blended into one ADSI object.
  • Independent. Extension writers can independently extend ADSI without prior knowledge of existing extensions.

Consider this scenario, a corporate developer or an ISV needs to develop a backup program. This backup program allows an administrator to back up all computers in the organizational unit he or she manages. With an ADSI extension, the following script is possible:

Dim ou as IADsContainer
Set ou = GetObject("LDAP://OU=Sales, DC=ArcadiaBay, DC=COM")
ou.Filter = Array("computer")
For each comp in ou
   Debug.Print comp.Get("dnsHostName")
   Debug.Print comp.LastBackUp
   comp.BackUpNow
Next

LastBackUp is a property and BackUpNow is a method that the extension writer provides. The code illustrates the benefits for both extension consumers and providers. The extension writer does not have to reinvent a new way of filtering, searching, and accessing the directory. The extension consumer, on the other hand, does not have to re-learn a new programming paradigm. New methods and properties that the extension writer provides are viewed as if they are part of ADSI.


ADSI Extension Architecture

ADSI extensions are based on the COM aggregation model with several enhancements. Extensions still must satisfy all COM rules. Please consult the COM specification for more information.

For convenience, let's quickly review the COM aggregation model.

COM Aggregation Model

An aggregate, also known as an inner object, is an object that an aggregator creates. Your extension object is an aggregate.

An aggregator, also known as an outer object, is an object that creates an aggregate. ADSI is an aggregator.

The inner object delegates its IUnknown to the aggregator's IUnknown.

ADSI extensions add the following enhancements to COM aggregation in order to satisfy its requirements:

  • Allows each extension writer to extend ADSI objects independently. An extension writer may register its extension with ADSI and not worry about the existence of other extensions. In the COM aggregation model, the aggregator must know the aggregate's CLSID. ADSI relaxes this requirement by making itself act as the aggregator for all extensions. Therefore, instead of forming a layer of nested components, extensions are siblings to each other.
  • Allows one object, one IDispatch. From day one, Automation support has been one of the most important features of ADSI. Automation support is achieved because ADSI supports the IDispatch interface. Extension writers are also encouraged to support the IDispatch interface. However, there should be only one IDispatch interface on a given object. ADSI integrates and collects the many IDispatch interfaces from different extensions and presents them as one consistent IDispatch to the Automation controller. Each extension, when aggregated, must re-route its IDispatch calls to the IDispatch provided by ADSI.

All these solutions are possible because of services that the ADSI Object Manager provides, which reside on each ADSI provider.

The following figure shows the ADSI Extension Model architecture.

ADSI Extension Component Model

ADSI supports two levels of extension:

  • Early Binding Support. This is the first level of extension. An extension must support registration and implement new interfaces. The extension consumers must use tools or scripting hosts that support early binding, for example, Visual C++ , Visual Basic.
  • Late Binding Support. This happens when an extension satisfies all early binding requirements, and implements an additional interface, IADsExtension. Extension consumers can use any tool that is able to act as an Automation controller, such as the Windows Scripting Host, Active Server Pages, and HTML with VBScript.


Early Binding Support

To begin, let's look at a scenario with early binding support in place. Consider the following code snippet:

Dim x as IADsUser
Dim y as IADsExt1
Dim z as IADsExt2

Set x = GetObject("LDAP://CN=JSmith, OU=Sales,
                   DC=Arcadiabay,DC=COM")
x.SetPassword("newPassword")

Set y = x
y.MyNewMethod( "\\srv\public")
y.MyProperty = "Hello World"

Set z = y
z.OtherMethod()
z.OtherProperty = 4362

Debug.Print x.LastName

Set z = GetObject("LDAP://CN=Alice,OU=Engr,
                   DC=Arcadiabay,DC=COM")
z.OtherProperty = 5323

In this example, two extension components extend a user object. Each extension publishes its own interface(s). Each extension is not aware of the other extension's interface(s), only ADSI is aware of the existence of both extensions. Each extension will delegate its IUnknown to ADSI. ADSI will act as an aggregator for both extensions, and any other future extensions. Querying an interface from any extension, or from ADSI, will yield the same consistent result.

Let's look at the steps required to make an extension.

Step 1: Add Aggregation to Your Component

Follow the COM specification for adding aggregation to your component. In summary, you must honor the pUnknown requests to your component during CreateInstance, and delegate the pUnknown to the aggregator's IUnknown if the component is being aggregated.

Step 2: Register Your Extension

Now you must decide which directory class you want to extend. You will not use the same interfaces to accomplish this that you would use for an ADSI interface (for example, IADsUser, IADsComputer). Directory objects are persisted in the directory, while your extension and ADSI will be running on the client's machine. Directory object examples are user, computer, printQueue, serviceConnectionPoint, and nTDSService. You can add a new class in Active Directory™ and create a new extension for this new class as well.

You will use registry keys to associate a directory class name with the ADSI extension components. The following figure represents the existing registry layout, as a well as new keys.

  • A new key, called Extensions, contains a list of keys, each of which represents a class in the directory. Each class, for example, user, printQueue, or computer, maintains a list of sub-keys.
  • Each sub-key contains the CLSID of the ADSI extension component.
  • Each CLSID sub-key contains a string entry that allows multiple values. You should only list the interfaces that participate in the aggregation.

Note Extension objects are still required to register standard COM keys.

HKLM
|__Software
|___Arcadiabay
|____ADS

|__Providers

|__LDAP

<continue from above>

LDAP

|__Extensions
|___<ClassNameA>
|____<CLSID of ExtensionA1>

Interfaces(REG_MUTI_SZ) {List of Interfaces)

|____<CLSID of ExtensionA2>

Interfaces(REG_MUTI_SZ) {List #1 of Interfaces)

|____ <ClassNameB>

|____<CLSID of ExtensionB1>

Interfaces(REG_MUTI_SZ) {List of Interfaces)

|____<CLSID of ExtensionB2> {List of Interfaces}

Interfaces(REG_MUTI_SZ) {List #1 of Interfaces)

Example

[HKEY_LOCAL_MACHINE\SOFTWARE\Arcadiabay\ADs\Providers\LDAP\Extensions\printQueue\{9f37f39c-6f49-11d1-8c18-00c04fd8d503}]
"Interfaces"={466841B0-E531-11d1-8718-00C04FD44407} {466841B1-E531-11d1-8718-00C04FD44407}

The list of interfaces from Extension1 can be different from that of Extension2. The object, when it's alive, will support the interfaces of the aggregator (ADSI) and all the interfaces provided by the aggregate's Extension1 and Extension2. Resolution of conflicting interfaces (the same interface supported by both the aggregator and the aggregates or by multiple aggregates) is determined by priorities of the extensions.

You may also associate your CLSID extension with multiple object class names. For example, your extension can extend both the user and contact objects.

Note Extension behavior is added per object class, not per object instance.

It is best to register your extensions, as you would any other COM components, with a call to the DllRegisterSvr function. You should also provide a facility to unregister your extension with the DllUnregisterServer function.

Here is an example for how to register an extension:

///////////////////////////////////////////
// Register the class
///////////////////////////////////////////
hr = RegCreateKeyEx( HKEY_LOCAL_MACHINE,
                 _T("SOFTWARE\\Arcadiabay\\ADs\\Providers\\LDAP\\Extensions\\User\\{E1E3EDF8-48D1-11D2-B22B-0000F87A6B50}"),
 0,
 NULL,
 REG_OPTION_NON_VOLATILE,
 KEY_WRITE,
 NULL,
 &hKey,
 &dwDisposition );

///////////////////////////////////////////
// Register the Interface
///////////////////////////////////////////
const TCHAR szIf[] = _T("{E1E3EDF7-48D1-11D2-B22B-0000F87A6B50}");

hr = RegSetValueEx( hKey, _T("Interfaces"), 0, REG_MULTI_SZ, (const BYTE *) szIf, sizeof(szIf) );

RegCloseKey(hKey);
return S_OK;

}


Getting ADSI Interfaces From Your Extension

An extension often needs to get information from the directory object it binds to. For example, an extension for computer may want to get the dnsHostName of the current object from the directory. This can be easily achieved by issuing a QueryInterface call on the IUnknown interface for the aggregator.

Example:

HRESULT hr;
IADs *pADs; ' ADSI Interface to get /set attributes

hr = m_pOuterUnk->QueryInterface(IID_IADs,(void**)&pADs);

if ( SUCCEEDED(hr) )
{
    VARIANT var;
    VariantInit(&var);
    hr  = pADs ->Get(L"dnsHostName", &var);
    printf("%S\n", V_BSTR(&var));
    VariantClear(&var);
    pADs->Release();
}

You should release the interface immediately after using it. Notice the call to pADs->Release in the code above. If the extension has an open reference to the aggregator, you have created a circular reference and the aggregator cannot release the extension. Therefore, the aggregator cannot be released and the result will be memory leaks in your application.


ADSI Extension Type Libraries

The type libraries for extensions are not merged. ADSI clients must specifically include the type library for each extension. The type library is needed for Visual Basic to enable early bindings. Clients written in C/C++ can call the QueryInterface method on the extension interfaces defined in the extension's header files.


ADSI and Extensions: What's Happening Under the Hood?

This is how ADSI interacts with extensions.

  • Something binds to an ADSI directory object. For example, LDAP://CN=Jsmith, OU=Sales, DC=ArcadiaBay, DC=COM.
  • ADSI finds that the object is in the class called user.
  • ADSI does a lookup in the registry and finds the extension CLSIDs for user. (Note: ADSI actually caches this information.)
  • Something calls the QueryInterface method for IID_IMyExtension. ADSI searches the interfaces associated with the user object, starting with its own interfaces, then looking at extension interfaces.
  • If a match is found, ADSI creates an instance of the component that supports IID_IMyExtension, and calls QueryInterface for the extension. The resulting interface is returned to the consumer.
  • The consumer uses this interface to call the interface's methods.
  • Next, the client calls QueryInterface for IID_IYourExtension, which is in a different component. This component will delegate this QueryInterface call to the IUnknown interface of the aggregator, which happens to be ADSI itself.
  • Once again, ADSI searches the interfaces and creates the component instance.


Late Binding Support

Let's revisit our scenario. When late binding support is in place, every function call must go through the ADSI IDispatch interface, before it is re-routed to the appropriate extension. Consider the following code snippet:

Set x = GetObject("LDAP://CN=JSmith, OU=Sales,
                   DC=Arcadiabay,DC=COM")
x.SetPassword("newPassword")

x.MyNewMethod( "\\srv\public")
x.MyProperty = "Hello World"

x.OtherMethod()
x.OtherProperty = 4362

Debug.Print x.LastName

Notice that there are no explicit calls to the QueryInterface method to get to the extensions. The extensions must re-route their IDispatch calls to the ADSI IDispatch interface. ADSI makes the decision and resolves any conflicts that occur, then it re-routes back to the appropriate extension using an interface called IADsExtension. Therefore, any extension that supports late binding must implement IADsExtension.


IADsExtension Interface

The IADsExtension interface is defined as follows:

IADsExtension : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE Operate(
            /* [in] */ DWORD dwCode,
            /* [in] */ VARIANT varData1,
            /* [in] */ VARIANT varData2,
            /* [in] */ VARIANT varData3) = 0;

        virtual HRESULT STDMETHODCALLTYPE PrivateGetIDsOfNames(
            /* [in] */ REFIID riid,
            /* [in] */ OLECHAR __RPC_FAR *__RPC_FAR *rgszNames,
            /* [in] */ unsigned int cNames,
            /* [in] */ LCID lcid,
            /* [out] */ DISPID __RPC_FAR *rgDispid) = 0;

        virtual HRESULT STDMETHODCALLTYPE PrivateInvoke(
            /* [in] */ DISPID dispidMember,
            /* [in] */ REFIID riid,
            /* [in] */ LCID lcid,
            /* [in] */ WORD wFlags,
            /* [in] */ DISPPARAMS __RPC_FAR *pdispparams,
            /* [out] */ VARIANT __RPC_FAR *pvarResult,
            /* [out] */ EXCEPINFO __RPC_FAR *pexcepinfo,
            /* [out] */ unsigned int __RPC_FAR *puArgErr) = 0;
    };

The aggregator (ADSI) calls the IADsExtension::Operate method. The extension should interpret the dwCode parameter and each varData parameter, according to the provider's documentation.

The aggregator (ADSI), calls the IADsExtension::PrivateGetIDsOfNames method. It is called after ADSI determines the extension to service the dispatch. The extension could use the type information for getting the DISPID, that is, using the DispGetIDsOfNames function.

ADSI normally calls the PrivateInvoke method after calling the PrivateGetIDsOfNames function. The extension should call the actual method that it implements. Alternatively, the extension can use type information and call the DispInvoke function.

All parameters have the same meaning as the parameters in the standard IDispatch::Invoke method.


IADsExtension Usage

IADsExtension is an optional interface. The extension writer should implement this interface when at least one of the following conditions is met:

  1. The extension component needs an initialization notification as defined by ADSI_EXT_dwCode in the Operate method.
  2. The extension supports a dual or dispatch interface.

If an extension component supports IADsExtension for reason number 1 only, the IADsExtension::PrivateGetIDsOfNames and IADsExtension::PrivateInvoke methods can simply return E_NOTIMPL. On the other hand, if an extension component supports this interface for reason number 2 only, the Operate method can simply ignore the data and return an HRESULT of E_NOTIMPL.

The following is an example of an extension implementing IADsExtension:

STDMETHOD(Operate)(ULONG dwCode, VARIANT varData1, VARIANT varData2, VARIANT varData3)
{
    HRESULT hr = S_OK;
    switch (dwCode)
    {
    case ADS_EXT_INIT:
         // you can prompt for a credential if you like
         // MessageBox(NULL, "INITCRED", "ADsExt", MB_OK);
          break;
    default:
          hr = E_FAIL;
          break;
    }
    return hr;
}

STDMETHOD(PrivateGetIDsOfNames)(REFIID riid, OLECHAR ** rgszNames, unsigned int cNames, LCID lcid, DISPID  * rgdispid)
{
      if (rgdispid == NULL)
      {
        return E_POINTER;
      }
    return  DispGetIDsOfNames(m_pTypeInfo, rgszNames, cNames, rgdispid);
}

STDMETHOD(PrivateInvoke)(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS * pdispparams, VARIANT * pvarResult, EXCEPINFO * pexcepinfo, UINT * puArgErr)
{
 return DispInvoke( (IHelloWorld*)this,
           m_pTypeInfo,
        dispidMember,
        wFlags,
        pdispparams,
        pvarResult,
        pexcepinfo,
        puArgErr );
}


Supporting Dual or Dispatch Interfaces

Like the dispatch interface, all dual interfaces must inherit from IDispatch, which delegates all of its IDispatch functions (GetIDsOfNames, Invoke, GetTypeInfo, GetTypeInfoCount) back to the IDispatch of the aggregator (ADSI). To execute the delegation, an extension object should query for the IDispatch of the aggregator (IID_IDispatch, or IID_IADs) from m_pOuterUnknown, and release the pointer after use.

If your extension can be a standalone component, you may want check if it is being aggregated. If so, you should re-route the dispatch functions to the IDispatch of the aggregator, otherwise you can call your internal implementation of IDispatch, or you can call your implementation of IADsExtension.

The following example shows how to re-route the IDispatch call to the IDispatch of the aggregator:

////////////////////////////////////////////////////
// Delegating IDispatch Methods to the aggregator
////////////////////////////////////////////////////
STDMETHODIMP MyExtension::GetTypeInfoCount(UINT* pctinfo)
{
    IDispatch *pDisp;
    HRESULT    hr;
    hr = OuterQueryInterface( IID_IDispatch, (void**) &pDisp );
    if ( SUCCEEDED(hr) )
    {
        hr = pDisp->GetTypeInfoCount( pctinfo );
        pDisp->Release();
    }
    return hr;
}

STDMETHODIMP MyExtension::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo** pptinfo)
{
    IDispatch *pDisp;
    HRESULT    hr;
    hr = OuterQueryInterface( IID_IDispatch, (void**) &pDisp );
    if ( SUCCEEDED(hr) )
    {
        hr = pDisp->GetTypeInfo( itinfo, lcid, pptinfo );
        pDisp->Release();
    }

    return hr;
}

STDMETHODIMP MyExtension::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid)
{
    IDispatch *pDisp;
    HRESULT    hr;
    hr = OuterQueryInterface( IID_IDispatch, (void**) &pDisp );
    if ( SUCCEEDED(hr) )
    {
        hr = pDisp->GetIDsOfNames( riid, rgszNames, cNames, lcid,
                 rgdispid);
        pDisp->Release();
    }

    return hr;

}

STDMETHODIMP MyExtension::Invoke(DISPID dispidMember, REFIID riid,
        LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT*
                pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
    IDispatch *pDisp;
    HRESULT    hr;
    hr = OuterQueryInterface( IID_IDispatch, (void**) &pDisp );
    if ( SUCCEEDED(hr) )
    {
        hr = pDisp->Invoke( dispidMember, riid, lcid, wFlags,
                 pdispparams, pvarResult, pexcepinfo, puArgErr);
        pDisp->Release();
    }

    return hr;
}

Extension writers are strongly encouraged to support dual interfaces instead of dispatch interfaces in their extension objects. A dual interface allows a client to have faster access, as long as vtable access is enabled in the client (please see Late Binding vs. Vtable Access in the ADSI Extension Model). Based on our current model, implementing dual interfaces should not be more difficult than implementing dispatch interfaces.


Late Binding: What's Happening Under the Hood?

Here is what is happening in the late binding case:

  1. Something binds to an ADSI directory object. For example, LDAP://CN=Jsmith, OU=Sales, DC=ArcadiaBay, DC=COM binds using COM late binding. This causes ADSI to call the QueryInterface method on the IDispatch interface.
  2. ADSI finds an object in the user class and creates an object that supports the appropriate interfaces (such as IADs, IADsUser).
  3. ADSI performs a lookup in the registry and finds extension CLSIDs for user. (Note: ADSI actually caches this information.)
  4. Something makes a call to MyNewMethod. ADSI looks up its dispatch ID and the dispatch IDs for other ADSI extensions. ADSI then finds the extension that serves this call, and calls the IADsExtension interface for that extension.
  5. The extension executes the function.
  6. Now, the client writer invokes YourNewMethod using the IDispatch interface for the current extension. The extension's IDispatch implementation delegates back to the IDispatch for ADSI.
  7. The IDispatch for ADSI again searches for the appropriate extension (or itself), then it calls the appropriate extension using the IADsExtension interface for that extension.


Revisiting COM Aggregation Rules with ADSI Extensions

The following is a quick review of COM aggregation rules along with ADSI extension rules.

  • The CreateInstance method returns a pointer to an IUnknown interface which does not delegate any function calls to the aggregator.
  • The IUnknown::QueryInterface method returns pointers to the interfaces it supports, and errors for interfaces it does not support.
  • The IUnknown::AddRef method increments the reference count on the aggregated extension object itself.
  • The IUnkown::Release method decrements the reference count on the aggregated extension object itself and destroys itself when the reference count is 0.
  • The extension object should store the IUnknown pointer of the aggregator (such as m_pOuterUnknown) during the implementation of the CreateInstance method.
  • All interfaces that the extension object supports, including IADsExtension, should inherit from IUnknown, which delegates all function calls back to the aggregator.

    IUnknown::QueryInterface calls m_pOuterUnknown->QueryInterface

    IUnknown::AddRef calls m_pOuterUnknown->AddRef

    IUnknown::Release calls m_pOuterUnknown->Release

Extension writers can choose any internal implementation they prefer as long as they obey standard COM aggregation rules. Please note that an extension object does not have to work as a standalone object. Extensions are designed to work as aggregates. However, an extension can be written to work as both a standalone object and as an aggregate.

In addition to standard COM aggregation support, an extension object may support IADsExtension for more advanced features. If late binding is supported, the extension should do the following:

  • Delegate functions for IDispatch back to the aggregator.
  • Implement the IDispatch functionality in IADsExtension.


What Does a Client See?

  • A client sees ADSI and all of its extension objects as one object.
  • An ADSI client sees one IDispatch interface which handles all the dual and dispatch interfaces in the object, whether the dual or dispatch interface is implemented by the aggregator in the provider or by an extension. (Please see Resolution of Function/Property Name Conflicts in Automation.)
  • ADSI does not expose any type information through the IDispatch::GetTypeInfo or IDispatch::GetTypeInfoCount methods. ADSI provides type information through the type library.


Resolution of Multiple Aggregation Components Supporting the Same Interface

It's highly unlikely that two extensions expose the same interface to ADSI. If this happens, however, the following rules apply:

  • If an interface, such as IFoo, is supported by both the aggregator (ADSI) and any extension objects, QueryInterface will always return the IFoo for ADSI.
  • If an interface, such as IFoo, is not supported by the aggregator (ADSI) but is supported by more than one extension object, QueryInterface will always return the IFoo of the first extension object listed in the registry which supports the interface.

Please note that the ordering of components in the registry will also affect the resolution of name conflicts in Automation (please see Resolution of Function/Property Name Conflicts in ADSI Extension Model).


Late Binding vs. Vtable Access in the ADSI Extension Model

A dual interface allows direct vtable access to all its functions while a dispatch interface does not. A C/C++ client can query for a dual interface pointer and use direct vtable access to invoke its functions. This provides faster access than invoking the function using the IDispatch::GetIDsOfNames and IDispatch::Invoke functions. This is especially true in our extension model, since all dual interfaces in an extension object must delegate their GetIDsOfNames and Invoke functions back to the aggregator (ADSI) first. The aggregator then needs to perform extra internal steps to sort out which extension object (possibly including the aggregator itself) provides support for the function being called and redirect the call to the appropriate object.

Visual Basic also invokes a dual-interface function using direct access to a vtable, if it has a pointer to the interface and access to type information from the type library. ADSI clients written in VB can specify a pointer to a dual interface, for example, IADsDualInf, explicitly, and thus enable vtable access to functions in the interface.

Example

Dim inf as IADsDualInf

Set inf = GetObject(…) 'an object supporting IADsDualInf

inf.Func1 'IADsDaulInf::Func1() will be invoked through
    'direct vtable access

Since an IDispatch interface does not support vtable access, this example does not apply. In other words, a dispatch function is always invoked through the IDispatch::GetIDsOfNames and IDispatch::Invoke functions only.

Current releases of VBScript and JavaScript also do not support vtable access. Therefore, a dual interface in a VBScript or JavaScript environment behaves like a dispatch interface.


Resolution of Function/Property Name Conflicts in Automation in Extensions

In this section, an object means the object, as a whole, as an ADSI client sees it. In other words, ADSI and all of its extensions.

If two or more dual IDispatch interfaces in an object support a property or method of the same name, for instance, Func1, invocation will be determined using the following criteria:

  1. If the client has a pointer to one of the dual interfaces which support a method called Func1, AND,
  2. If the Automation environment supports vtable access.

In the Late Binding vs. Vtable Access in the ADSI Extension Model section, there was a brief explanation about how a client can add a pointer to a dual interface, and a description of the types of environments that support vtable access. Further details can be obtained from guides for COM/OLE, Visual Basic, Visual Basic for Applications, VBScript, and JavaScript.

If both conditions are true, that is, a client has a pointer to a dual (only) interface, IDuallInf1, which supports a function called Func1, and the Automation environment supports vtable access, IDualInf1::Func1 will be invoked directly through ADSI vtable access.

If condition 1) or 2) fails, IDispatch::GetIDsOfNames and IDispatch::Invoke will be called to invoke Func1. Since all extension objects redirect the IDispatch functions back to the aggregator, the aggregator controls which Func1 will be invoked. The rules are as follows:

  • If any interface, and there will only be one, if any, in the aggregator (ADSI) supports a function called Func1, the aggregator will invoke its own Func1.
  • Otherwise, the aggregator will go through each of its extensions, in the order listed in the registry, and find the first extension which implements a function called Func1. It is possible, but highly unlikely, that multiple dual IDispatch interfaces in this first extension have a function called Func1. It is up to the extension to determine which Func1 should always be invoked in Automation.


Example Code for Resolving Function Name Conflicts

Consider the following assumptions:

  • IADs0 does not support Func0.
  • IADs1 supports Func1 and Func0.
  • IADs2 supports Func2 and Func0.

All three interfaces are dual interfaces.

IADs0 : IDispatch
{
  OtherFunc();
}

IADs1 : IDispatch
{
 Func0()
 Func1();
}

IADs2 : IDispatch
{
 Func0()
 Func2();
}

[Visual Basic]

Dim myInf1 as IADs1

myInf1.Func1  ' IADs1::Func1 is invoked using direct vtable access

myInf1.Func2  ' IADs2::Func2 is invoked using GetIDsOfNames/Invoke

Note that even though IADs1 does not support Func2, an ADSI client sees one IDispatch which supports all the dual and dispatch interfaces in our model. Thus, the ADSI client can directly call Func2 using myInf1.Func2 without figuring out which interface supports Func2.

myInf1.Func2

Note that both IADs1 and IADs2 have a function called Func0, but IADs1::Func0 is invoked directly using vtable access, because the following apply to the client:

  1. It has a pointer to dual interface IADs1, which has a function called Func0, AND,
  2. Visual Basic supports direct vtable access, assuming that type information is available through the type library.

In the next example, the client has a dual interface pointer to IADs2 instead of IADs1. Therefore, IADs2::Func0 is invoked using direct vtable access.

Dim myInf2 as IADs2
Set myInf2 = myInf1 ' Querying for pointer to IADs2
myInf2.Func0

Again, in the next example, both IADs1 and IADs2 have a function called Func0, but this time the client has a pointer to a dual interface, IADs0, which does not have a function called Func0. Therefore, no direct vtable access can be performed. Instead, IDispatch::GetIDsOfNames/Invoke are called to invoke Func0.

Dim myInfNone as IADs0
Set myInfNone = myInf1    ' the aggregated object which
' supports both IADs1 & IADs2
myInfNone.Func0

Consider these two cases:

  1. IADs1 and IADs2 are implemented by two COM components, Ext1 and Ext2, respectively. If Ext1 comes before Ext2 in the registry, IADs1::Func0 is invoked. However, if Ext2 comes first in the registry, IADs2::Func0 is invoked.
  2. If IADs1 and IADs2 are implemented by the same extension object, Func0 is always invoked by the extension's IADsExtension::PrivateGetIDsOfNames and PrivateInvoke methods.

It's completely up to the extension developer to determine how to resolve conflicts of functions (or properties) of different dual IDispatch interfaces that have the same name in an extension. The implementation of IADsExtension::PrivateGetIDsOfNames and PrivateInvoke methods should resolve this conflict. For example, if you use IFoo::Func1 and IMoreFoo::Func1, where IFoo and IMoreFoo are dual IDispatch interfaces supported by the same extension object. The PrivateGetIDsOfNames and PrivateInvoke methods must determine which Func1 should always be called.

The same applies to conflicting DISPIDs in different dual or IDispatch interfaces.

For example, the DISPID of IFoo::Y is 2 in the file ifoo.odl (or ifoo.idl). The DISPID of IMoreFoo::X is also 2 in imorefoo.odl. IADsExtension::PrivateGetIDsOfNames must return a unique DISPID (within the extension itself) for each, instead of returning the same DISPID for both.

ADSI resolves the first problem by not supporting multiple interfaces with conflicting function or property names. It resolves the latter problem by adding a unique (within the same extension object) interface number to the unused bits of the DISPID.


More on the Resolution of Automation Conflicts: Same Function Name but Different Parameters

In Resolution of Function/Property Name Conflicts in Automation in Extensions, there was discussion about how to resolve function name conflicts, that is, function names that use the same function name and same parameter list (number, type and order), when it occurs in Automation. But what if two functions have the same name but different parameters? Unfortunately, if an ADSI client invokes the function using IDispatch::GetIDsOfNames without using multiple names to specify the parameters, the ADSI extension model cannot disambiguate the functions. Based on the resolution scheme discussed in Resolution of Function/Property Name Conflicts in Automation in Extensions, the first extension in the registry which supports this function through one of its interfaces will have its version of this function invoked, and the call may fail or yield incorrect results.

For example:

Extn1 (first in the registry under class CA – higher priority) supports IInterface1.

Extn2 (third in the registry under class CA – lower priority) supports IInterface2.

IInterface1 supports Foo1(int param1, int param2).

IInterface2 supports Foo1(int param1)

An ADSI client has an IDispatch interface pointer to an object of class CA. It wants to invoke IInterface2::Foo1. If the client calls pDispatch->GetIDsOfNames(IID_NULL, rgszNames, 1, MY_LCID, rgDispId) by just storing the function name "Foo1" in rgszNames[0], then IInterface1::Foo1, instead of the desired IInterface2::Foo1 will be invoked, and the function will fail since the number of parameters are different.

To minimize this problem, extension developers can prefix their function names with their own specific identifiers and avoid interface designs that use functions of the same name but different parameters.

If a name conflict does occur, ADSI clients can avoid the problem by direct vtable access if the interface is a dual interface (see the discussion in the previous section). If direct vtable access is not possible, ADSI clients should call IDispatch::GetIDsOfNames with multiple names, specifying the function names as well as the parameters in the array rgszNames described previously.

Visual Basic 5 does not call the IDispatch::GetIDsOfNames function with multiple names. That is, it only passes the function name to GetIDsOfNames, but not arguments. However, Visual Basic 5 allows users to invoke a function by direct vtable access (see the previous discussion), instead of invoking the IDispatch::GetIDsOfNames function if the interface is a dual interface. Developers should use direct vtable access, if possible.


Using ADSI with NDS Providers

Servers providing Novell NetWare® Directory Services are accessed with ADSI by binding to any of the ADSI objects of NDS. This section provides a few examples of some common operations.


Setting Up Gateway and Client Services for NetWare

Before you set up ADSI to work with NDS, the Novell NetWare gateway and client services must be installed properly, and the Novell NDS Server must be accessible from the network.

Note Adsnds.dll and Activeds.dll must be installed on the client system in order for that client to be able to have access to the provider.

To Install gateway and client services on Windows NT 4.0

  1. Click the Start button, go to Settings, and click Control Panel.
  2. Double-click Network. The Network dialog box opens.
  3. Click the Service tab.
  4. Select Gateway (and Client) Services for NetWare.
  5. Click OK to install, then reboot.
  6. Reinstall the appropriate Windows NT Service Pack, then reboot.
  7. Install (or reinstall) ADSI 2.5 or higher (http://www.microsoft.com/adsi).

Note You must install ADSI after you have installed the Gateway and Client Service for NetWare.

To Install gateway and client services on Windows 2000

  1. Click the Start button, go to Settings, and click Dial-up Connection.
  2. Click Local Network.
  3. Click Client for Microsoft Network.
  4. Click Install.
  5. Select the Client icon, then click Add.
  6. Select Gateway (and Client) Services for NetWare.
  7. Install.


NDS Example Code

To connect to an NDS Server, you bind to the NDS object using an NDS ADsPath. This example shows how to enumerate the various ADSI objects of an NDS server.

Dim dso
Dim obj
Dim usrName
Dim password
Dim serverName

servername = "ntmarst2"
userName = "supervisor.ntmarst2"
password = "secretpwd"

'--bind to the server
Set dso = GetObject("NDS:")
Set cont = dso.OpenDSObject("NDS://" & serverName,userName,password,0)

'--enumerate the server's objects
For Each obj In cont
    Debug.Print obj.Name & " (" & obj.Class & ")"
Next

To retrieve and modify a User object's attribute, bind to the NDS object and change the desired property value using the Get and Put interfaces. This example changes a particular user's last name to "Johnson".

Path = "O=NTMARST2/CN=benny"
ADsPath = "NDS://" & serverName & "/" & Path
Set usr = dso.OpenDSObject(ADsPath, userName, password, 0)
Debug.Print usr.Get("Surname")
usr.Put "SurName", "Johnson"
usr.SetInfo

This example creates a new user with a username of "bagheeraw".

Path = "O=NTMARST2"
ADsPath = "NDS://" & serverName & "/" & Path
Set cont = dso.OpenDSObject(ADsPath, userName, password, 0)
Set usr = cont.Create("user", "bagheeraw")
usr.Put "cn", "Bagheera"
usr.Put "Surname", "White"
usr.SetInfo

This example searches an NDS server for a user with a surname of "Bagheera".

ADsPath = "NDS://" & serverName
Set con = CreateObject("ADODB.Connection")
con.Provider = "ADsDSOObject"
con.Properties("User ID") = userName
con.Properties("Password") = password
con.Open "ADSI"

Set com = CreateObject("ADODB.Command")
Set com.ActiveConnection = con
com.CommandText = "SELECT ADsPath, 'Object Class' FROM '" & ADsPath & "' WHERE Surname='Bagheera'"
Set rs = com.Execute

While Not (rs.EOF)
   Debug.Print rs.Fields("ADsPath")
   rs.MoveNext
Wend


Using ADSI with NWCOMPAT Providers

Servers providing Novell NetWare 3.x services are accessed with ADSI by binding to any of the ADSI objects of NWCOMPAT. This section provides a few examples of some common operations.


Setting Up Gateway and Client Services for NetWare 3.x

Before you set up ADSI to work with NWCOMPAT, the Novell NetWare gateway and client services must be installed properly. For more information, see Setting Up Gateway and Client Services for NetWare.

Next, you will need to make the Novell Netware Server accessible from the network.


NWCOMPAT Example Code

If you have not logged on to the NetWare Server, you must use the net use command to log on before using any ADSI NWCOMPAT services. For example,

net use \\nwserver /U:mydom\jsmith

This is required because IADsOpenObject::OpenDSObject is not supported in the current release of the NWCOMPAT provider.

This example enumerates the various ADSI objects of an NWCOMPAT server.

servername = "ntmarst2"
adsPathName = "NWCOMPAT://" & serverName
Set cont = GetObject(adsPathName)

'--enumerate the server's objects
For Each obj In cont
    Debug.Print obj.Name & " (" & obj.Class & ")"
Next

This example creates a new user with a username of "alicew".

adsPath = "NWCOMPAT://" & serverName
Set cont = GetObject(adsPath)
Set usr = cont.Create("user", "alicew")
usr.SetInfo

This example changes a particular user's full name to "Alice I. Wonderland".

objPath = "alicew,user"
adsPath = "NWCOMPAT://" & serverName & "/" & objPath
Set usr = GetObject(adsPath)
usr.FullName = "Alice I. Wonderland"
usr.SetInfo

Searching is not supported in NWCOMPAT. You can, however, use the IADsContainer::put_Filter method to limit the type of object classes to be included in the enumeration.

adsPath = "NWCOMPAT://" & serverName
Set con = GetObject(adsPath)
con.Filter = Array("user", "group") 'Show user and group

For Each acct In con
    Debug.Print acct.Name & " (" & acct.Class & ")"
Next


ADSI Utility Interfaces

ADSI provides several utility interfaces for ADSI objects that can help in accessing and maintaining the Active Directory. These interfaces are:

  • IADsDeleteOps
  • IADsObjectOptions
  • IADsPathname


IADsDeleteOps Interface

The IADsDeleteOps interface is used primarily in supervisory and maintenance routines for the underlying directory store. It is capable of deleting both leaf objects and entire subtrees in a single operation.

If this interface is not supported, deleting an object from Active Directory requires that each contained object be recursively enumerated and deleted individually. With this interface, any container object — along with all of its contained objects and subobjects — can be deleted with a single call.

This example deletes the entire "Eng" (Engineering) group from the Users list on an LDAP server.

HRESULT hr;
IADsOpenDSObject *pDSO;

// bind to the LDAP provider
hr = ADsGetObject(L"LDAP:", IID_IADsOpenDSObject, (void **) &pDSO);
if (SUCCEEDED(hr))
{
    // get the dispatch interface pointer using Admin credentials
    IDispatch *pDisp;
    hr = pDSO->OpenDSObject(
             L"LDAP://primedom/CN=Eng,CN=Users,DC=ArcadiaBay,DC=Com",
             L"Administrator",
             L"passwordgoeshere",
             ADS_SECURE_AUTHENTICATION, &pDisp);
    pDSO->Release();

    if (SUCCEEDED(hr))
    {
        // get the DeleteOps interface pointer
        IADsDeleteOps *pOps;
        hr = pDisp->QueryInterface(IID_IADsDeleteOps, (void**) &pOps);
        pDisp->Release();

        if (SUCCEEDED(hr))
        {
            // delete the whole group
            pOps->DeleteObject(0);
            pOps->Release();
        }
    }
}


IADsObjectOptions Interface

The IADsObjectOptions interface gives direct access to setting and retrieving provider-specific options. This is currently supported only in Active Directory.

One of the Active Directory object options is to return the host name of a server. This example routine uses the interface to retrieve the host name of the global catalog server.

HRESULT GetGCServerName(VARIANT *vGCServer)
{
    HRESULT hr, hre;
    IADsContainer *pContainer = NULL;
    IUnknown *pUnk = NULL;
    IEnumVARIANT *pEnum = NULL;
    IDispatch *pDisp = NULL;
    IADsObjectOptions *pOpt = NULL;
    VARIANT var;
    ULONG lFetch;

    // bind to the global catalog using a serverless bind
    hr = ADsOpenObject(L"GC:", NULL, NULL,
                       ADS_SECURE_AUTHENTICATION,
                       IID_IADsContainer, (void**) &pContainer );
    if (FAILED(hr))
        return (hr);

    hr = pContainer->get__NewEnum(&pUnk);
    if (SUCCEEDED(hr))
    {
        hr = pUnk->QueryInterface(IID_IEnumVARIANT, (void**) &pEnum);
        if (SUCCEEDED(hr))
        {
            // now enumerate - there is only one item here
            hr = pEnum->Next(1, &var, &lFetch);
            if (SUCCEEDED(hr))
            {
                while (SUCCEEDED(hr))
                {
                    if (lFetch == 1)
                    {
                        pDisp = V_DISPATCH(&var);
                        hre = pDisp->QueryInterface(
                                          IID_IADsObjectOptions,
                                          (void**)&pOpt);
                        if (pDisp)
                            pDisp->Release();
                    }
                    VariantClear(&var);
                    hr = pEnum->Next(1, &var, &lFetch);
                }
                // S_FALSE means we read all of the row properly
                if (hr == S_FALSE)
                    hr = hre;
            }

            if (SUCCEEDED(hr))
            {
                // we have a valid pOpt, so request the server name
                VariantInit(vGCServer);
                hr = pOpt->GetOption(ADS_OPTION_SERVERNAME,vGCServer);
            }
        }
    }

// clean up
    if (pOpt)
        pOpt->Release();
    if (pEnum)
        pEnum->Release();
    if (pUnk)
        pUnk->Release();
    if (pContainer)
        pContainer->Release();
    return (hr);
}


IADsPathname Interface

The IADsPathname interface is used to parse and modify various elements of an ADsPath. It also converts ADsPaths between various display formats.

This example extracts and returns just the server name from a valid ADsPath for display to the user in a maintenance utility.

HRESULT GetServerName(LPWSTR adsPath, LPWSTR *adsServer)
{
    HRESULT hr = S_OK;
    IADsPathname *pIADsPathname = NULL;

    // create the IADsPathname object
    hr = CoCreateInstance(CLSID_Pathname,
                          NULL,
                          CLSCTX_INPROC_SERVER,
                          IID_IADsPathname,
                          (void**) &pIADsPathname);
    if (FAILED(hr))
    {
        if (pIADsPathname)
            pIADsPathname->Release();
        return (hr);
    }

    // set the pathname
    hr = pIADsPathname->Set(adsPath, ADS_SETTYPE_FULL);
    if (SUCCEEDED(hr))
        // extract and return the server name
        hr = pIADsPathname->Retrieve(ADS_FORMAT_SERVER, adsServer);

    // clean up
    pIADsPathname->Release();
    return (hr);
}

This example helps to initialize a newly created ADSI object by setting the object's Distinguished Name property from its own ADsPath. Note that the calling routine is responsible for committing any changes to the underlying directory store by invoking the SetInfo method.

HRESULT SetDistinguishedName(IADs *pIADs)
{
    HRESULT hr = S_OK;
    LPWSTR bsADsPath, bsADsWDNPath;
    IADsPathname *pIADsPathname = NULL;

    // get the ADsPath for this object
    hr = pIADs->get_ADsPath(&bsADsPath);
    if (FAILED(hr))
        return (hr);

    // create the IADsPathname object
    hr = CoCreateInstance(CLSID_Pathname,
                          NULL,
                          CLSCTX_INPROC_SERVER,
                          IID_IADsPathname,
                          (void**) &pIADsPathname);
    if (FAILED(hr))
    {
        if (pIADsPathname)
            pIADsPathname->Release();
        return (hr);
    }
     // set the pathname
    hr = pIADsPathname->Set(bsADsPath, ADS_SETTYPE_FULL);
    if (SUCCEEDED(hr))
    {
        // convert the path to Distinguished Name format
        hr = pIADsPathname->Retrieve(ADS_FORMAT_WINDWOS_DN,
                                     &bsWDNPath);

        if (SUCCEEDED(hr))
        {
            // set the distinguished name property
            VARIANT var;
            VariantInit(&var);
            V_BSTR(&var) = bsWDNPath;
            V_VT(&var) = VT_BSTR;
            hr = pIADs->Put(L"distinguishedName", var);
            VariantClear(&var);
        }
    }

    // clean up
    pIADsPathname->Release();
    return (hr);

}


ADSI for Clients

Active Directory™ Service Interfaces are COM interfaces that isolate client applications from underlying network specific API calls to a directory service. Client applications that use these interfaces or the COM Automation objects that expose IADs and the other Active Directory Service interfaces benefit from the layered insulation of the underlying directory services.

You can write ADSI client applications in many languages, For the majority of administrative tasks, ADSI defines interfaces and objects accessible from Automation-compliant languages like Microsoft® Visual Basic®, Microsoft® Visual Basic® Scripting Edition (VBScript), and Java to the more performance and efficiency-conscious languages such as C and C++.

Smooth integration with Active Server Pages and VBScript make it easy to write Internet applications that access directory services. For integration with OLE DB applications, ADSI supplies an OLE DB provider by supporting a subset of the OLE DB query interfaces. For Internet applications, using scripting in .asp files (Active Server Pages) can create and manipulate ADSI objects on the server and display the results in a Web page. In Microsoft® Management Console, directory-service administration snap-ins can use ADSI to find directory services of interest. In short, Active Directory Service Interfaces can provide access to a broad and diverse set of directory services — including those not yet built.

For tasks that require high performance and low overhead, the ADSI architecture defines low-level, non-Automation interfaces accessible from languages like C and C++ that are little more than COM wrappers for on-the-wire protocols to a directory service.

Coding to the published interfaces allows your application to reach directory services for all installed ADSI providers and integrate the resulting information. Without recompiling or amending your code, your application can continue to access additional directory services on your network as new ADSI providers are installed.

The following figure shows how ADSI fits into an application environment. Whether the application is written in Java, Visual Basic, C/C++, VBScript, Microsoft® JScript™, or as a web application using Active Server Pages, Active Directory Service Interfaces provide a clean and easy-to-use access to the underlying directory services without having to use the native network APIs.

As shown in the preceding figure, non-Automation clients have access to all ADSI interfaces, including both pure COM interfaces with the naming convention IDirectoryXXX and Automation COM interfaces with the naming convention IADsXXX. Since clients predominantly request information from directory services, the ADSI flexible query model through OLE DB and IDirectorySearch is especially powerful.


ADSI Development Environment

The ADSI SDK contains the following files of interest to ADSI clients:

  • ADSI DLLs
  • ADSI OLE DB Provider DLLs
  • ADSI Provider DLLs for WinNT, LDAP, NDS, Novell NetWare® 3.x
  • ADSI libraries
  • ADSI type library for Automation controllers
  • ADSI .odl files
  • ADSI include files
  • ADSI .dbg files
  • ADSI .hlp file


Client Overview

If you understand the object model and ADSI architecture, you'll be able to easily take advantage of the range of features ADSI offers. The following sections discuss the specific features that are supplied by ADSI as they are important for writing client applications. Whether you are coding in Java, Visual Basic®, Microsoft® Visual Basic® Scripting Edition (VBScript), C, C++, or some other scripting or development language, the following sections discuss the concepts you should understand to gain the most from this set of interfaces. Each section supplies a brief introductory discussion and lists the items of interest. See Using ADSI Objects and Interfaces for detailed examples of each section.

  • ADSI Object Model
  • ADSI Architecture
  • ADSI Schema Model
  • ADsPaths
  • Interface Summary
  • Helper Functions
  • Binding to an Object
  • Accessing a Directory Service
  • Querying a Directory Service
  • ADSI Security Model
  • About Collections and Groups
  • About Provider-Specific Information


ADSI Object Model

ADSI objects represent any item within a directory service: computers, users, files, servers, printers, print queues, and more – elements that network administrators work with daily. ADSI defines different kinds of objects to represent different kinds of elements. Each object, as shown in the following figure, supports one or more COM interfaces that give you access to information about the object itself (called metadata) and information about what the object represents.

Because COM interfaces are logically connected sets of properties and methods, you can think of each interface as a handle to the object that allows you access only one set of logical functions at a time. In ADSI, the fundamental interfaces are:

IADs — Object Identification
Fundamental interface required on all ADSI objects. Supplies access to properties that describe the object itself (its metadata) including its definition in the ADSI Schema and access to properties and methods that manage the object's data in the property cache.
IADsContainer — Object Lifetime Management and Detection
Fundamental interface required on all ADSI container objects. Manages object creation, deletion, copying and moving, binding and enumeration.
IADsPropertyList — Object Property Management
Fundamental interface that optimizes managing an object's data in the property cache.
IDirectoryObject — Direct Object Access
Low-level object access for non-Automation clients. This interface bypasses the object property cache and provides a direct access to object properties. For more information, see IADs vs. IDirectoryObject Global Strategy.
IUnknown — COM Object Management
Required on all COM objects.
IDispatch — Type Library Information and Method Invocation.
Required on all Automation objects.

More complex ADSI objects may expose additional interfaces. For example, IADsCollection supports methods that manage collections of directory elements of the same data type. IADsGroup methods manage the special case collections of objects that support the IADsMembers interface. For providers that support it, the IDirectorySearch interface supports methods to query directory services. In addition, ADSI supplies interfaces that represent well-known logical and physical items. ADSI objects that represent users support IADsUser, those that represent computers support IADsComputer, and so on. Expanded explanations of ADSI objects can be found in IADs vs. IDirectoryObject Global Strategy. Not all providers implement all interfaces or all methods and properties on all interfaces. For more information, see provider-specific documentation in the ADSI reference pages in the Microsoft® Platform SDK.


ADSI Architecture

Many directory services are hierarchical in nature and thus lend themselves to a hierarchical object model. A natural way of displaying these relationships is to use the familiar browser tree view, where double-clicking any node expands the node to show what it contains. Both the object representation and the browser representation are used in the following section to illustrate ADSI features.

In the following object model figure, a top-level system object contains one Namespace object for every installed ADSI provider.

Each of the Namespace objects is itself a container that contains the top-level root nodes of every server, domain, or whatever other kinds of directory-system objects are defined as roots in each directory service.

The following figure shows browser representation of this same set of objects using the Visual Basic-based Dsbrowse.exe utility that comes with this SDK. The top-level ADSI object has been expanded to show the Namespace objects.

When you expand each Namespace object in the browser, the ADSI provider implementation of that Namespace object supplies the information you are requesting. This gives you access to both the power and the limitations of each directory service. For example, expanding the WinNT Namespace object yields the list of Domain objects or root nodes currently connected to the network. Expanding NDS and NWCOMPAT each also give the top-level root nodes on the network.

ADSI supplies a set of predefined objects and interfaces so that client applications can interact with directory services using a uniform set of methods. However, ADSI may not provide access to the complete functionality of a particular directory service. To help use the full power of each directory service, ADSI supplies a schema model that directory service providers and third-party software vendors can use to extend functionality beyond the interfaces ADSI offers.

The root-node container objects found within each provider Namespace object include an ADSI schema container object. This object contains the definition of all the functionality for that provider. For more information, see ADSI Schema Model.


ADSI Schema Model

A schema is like a dictionary in that it holds the definition of every type of object known to a directory service. ADSI client applications can browse through a schema to discover the features of any given ADSI implementation. In addition, ADSI supplies schema management interfaces that can be used to communicate with a directory service's underlying schema.

Some schemas are extensible and ADSI providers or third-party suppliers may choose to publish new interfaces or additional properties for existing interfaces there. ADSI clients use this information to determine what features are supported for each directory service.

There are three kinds of schema objects: classes, properties, and syntaxes, each respectively supporting the schema management interfaces IADsClass, IADsProperty, and IADsSyntax.

Note Class is an overloaded term in the software world. There are C++ classes, Java classes, COM classes, and ADSI classes. In this document, the word class, unless otherwise qualified, refers to a category or type of schema object.

ADSI abstracts the schema of every directory service and places it in every top-level root node in the Namespace object. To find out what classes a directory service supports on a given root-node, you enumerate the schema object and get a list of class objects, property objects and syntax objects. Details of this configuration are discussed in Using the ADSI Schema.


ADsPaths

Since the number of objects accessible from a directory service may be very large, you need some way to specify particular objects without naming collisions. ADsPaths enable you do just that both for a single provider and across multiple providers.

An ADsPath is a case-sensitive string guaranteed to identify uniquely an ADSI object on any given directory service. Because ADSI objects exist within the context of the namespace of the underlying directory service, part of the syntax of an ADsPath name is provider-specific.

The initial elements of the ADsPath string are the programmatic identifier (progID) of the ADSI provider, followed by "://" followed by whatever syntax is dictated by the provider namespace.

The following are examples of ADsPaths:

LDAP://ldapsvr/CN=TopHat,DC=DEV,DC=MSFT,DC=COM
LDAP://MyDomain.microsoft.com/CN=TopHat,DC=DEV,DC=MSFT,DC=COM
LDAP://cn=MyName,o=msft,c=us

WinNT://MyDomain/ComputerName,Computer
WinNT://MyDomain/UserAccount

NDS://MarsTree/O=MARS/OU=MARTIANS/CN=MyFavorite

NWCOMPAT://NWServer/MyNw3xPrinter

Using the LDAP namespace, you can specify the ADsPath either in an X500 DN (distinguished name) form starting with the CN tag or its hierarchical inverse starting with the O tag. Whichever form you use in the initial ADsPath determines its order.

Characters that have special meaning when referring to an ADsPath are listed in the following table.

Special Characters

Name | Character | Comment
Double quote | " | Used to quote any part of the ADsPath that may contain a special character so that the string is interpreted literally. For example, "CN=Name/Prefix"
Backward slash | \ | Escape character, used to precede special characters to signify they should be used as literals, including the '\' character and ',' (commas). For example, CN=Name\/Prefix
Forward slash | / | Component separator.
Right and Left angle bracket | <> | Delimits an ADsPath within another naming convention.

In order to delimit an ADsPath in a search specification or as part of an URL, use angle brackets. For example, "<WinNT://MyDomain/UserAccount>".

Some ADSI providers may have additional restrictions on the syntax because of their namespace requirements.

In addition to supporting ADsPaths, ADSI also supports COM globally unique identifiers (GUIDs), which provide a well-known way to establish a unique tag by which an object can be distinguished from all other objects ever created. For more information about the GUID structure, see the Microsoft® Platform SDK.


Interface Summary

A list of all Active Directory™ Service Interfaces can be found in ADSI Definitions, but in general, interfaces fall into several groups of functionality.

IADs, IADsContainer, and IADsPropertyList (along with its associated IADsPropertyEntry and IADsPropertyValue) are the fundamental interfaces that supply the foundation for interactions among ADSI objects. You can use their built-in house-keeping functions to keep track of names and hierarchical relationships, and their cache functions for improved performance.

The schema-management interfaces IADsClass, IADsProperty, and IADsSyntax identify the types of objects used to represent a specific directory service. You can use these interfaces to determine what types of objects are known to a specific directory service and to define and add new schema classes at run time.

You can use IADsMembers, IADsCollection and IADsGroup to define, enumerate, and manage collections of similar objects. Group memberships use are special collections of IADsMembers interfaces.

You can use the interfaces IADsAccessControlEntry, IADsAccessControlList, and IADsSecurityDescriptor to support access-control lists.

Use IADsOpenDSObject or the helper function ADsOpenObject to supply credentials to an operating system when first binding to a directory service object.

IDirectorySearch, available only to non-Automation clients, supports the ability to query a directory service and provides several practical means to guide the search for performance reasons. Because ADSI is an OLE DB provider, Automation clients can use the OLE DB-style queries discussed in Using the ADSI OLE DB Provider for Searches.

IDirectoryObject, available only to non-Automation clients, expands the set of supported syntaxes to a wider range, including provider-specific syntaxes. This interface also bypasses the overhead of ADSI housekeeping functions and provides direct, on-the-wire interactions with the directory service.

ADSI supplies implementations of interfaces, such as IADsUser , IADsDomain, and IADsOU, that represent a hierarchical grouping of the underlying data and services. By using these ADSI implementations, an application can maximize its functionality across all namespaces with installed ADSI providers on a network.

ADSI also supplies helper functions for C and C++ client applications.


Helper Functions

ADSI supplies several helper functions available to C and C++ developers. For a list of these functions, see ADSI Functions.

ADsGetObject, given a fully qualified distinguished name, finds the directory element specified, creates an ADSI object to represent it, and retrieves an interface pointer on that object. ADsOpenObject, in addition to performing the same function, also supplies a user name and password for credentials.

Listing the contents of a container or collection object requires a special type of object known as an enumerator. The IEnumXXXX interface on the enumerator object supplies the methods needed to list the collection of elements or objects within a container or collection. The helper functions ADsBuildEnumerator, ADsEnumerateNext, and ADsFreeEnumerator encapsulate the code needed to perform these operations. In addition, ADsBuildVarArrayInt and ADsBuildVarArrayStr are supplied to ease the task of building an array of integers or strings of type VARIANT. For more information, see enumeration examples in Automation Examples or COM Vtable Examples.


Binding to an Object

Binding to a directory object through ADSI is simple. In languages that support Automation, such as Java or Visual Basic®, use the GetObject function with a fully specified ADsPath.

Binding to user objects from different directory services
Dim MyObject1 As IADsUser
Dim MyObject2 As IADsUser
Set MyObject1 = GetObject("WinNT://MyDomain/MyAccount,User")
Set MyObject2 = GetObject("LDAP://MyLdapSvr/CN=MarySmith,DC=ArcadiaBay")

In C or C++, the binding looks very similar:

IADsUser *pMyObject1, *pMyObject2;
ADsGetObject(TEXT("WinNT://MyDomain/MyAccount,User"),
              IID_IADsUser,
              (void) ** &pMyObject1);
ADsGetObject(TEXT(
              "LDAP://MyLdapSvr/O=Internet/DC=Redmond/CN=MarySmith"),
              IID_IADsUser,
              (void) ** &pMyObject2);

In each case, the client application calls either ADsGetObject or Visual Basic® GetObject to obtain a pointer or a reference respectively, to an interface on an ADSI object that represents an end-user.

ADSI uses the ProgID in the first part of the string to load the appropriate ADSI provider DLL to process the bind request.

When credentials need to be established when passing a bind request to a directory service, you can use ADsOpenObject. In Automation environments, the Namespace object supports the IADsOpenDSObject interface to supply the same functionality.

The following Visual Basic code fragment supplies credentials when binding to an object:

Dim MyObject as Object
Dim MyNamespace as IADsOpenDSObject

Set MyNamespace = GetObject("LDAP:")
Set MyObject = MyNamespace.OpenDSObject("LDAP://MyLdapSvr/CN=MarySmith,DC=ArcadiaBay",       "Mary Smith","LandOfOz",0x1);

C and C++ programmers can use ADsOpenObject as shown below:

hr = ADsOpenObject(
        LDAP://MyLdapSvr/O=Internet,
        "Mary Smith",
        "LandOfOz",
        ADS_SECURE_AUTHENTICATION,
        IID_IADs,
        (void **)&pIADs
        );

For more information, see ADSI Component Interaction.


Accessing a Directory Service

Client applications frequently display directory service information through browsers and query functions. Several examples are included in this section to show ADSI client applications.

The following figure provides an example. Using the Visual Basic® browser Dsbrowse.exe, which is provided in the ADSI SDK, you can start at the top-level "ADs:" container and expand it. By double-clicking several container objects under the "WinNT:" namespace, you can see how the hierarchical object model works. The Namespace object contains two domain objects. The domain object"MYDOMAIN2" contains a user object, a computer object, and a group object names "Users."

The browser determines if an object is a container object by trying to enumerate it. If the attempt fails, the object contains no other objects.

If you already have the distinguished name for a directory object, you can use an application like the command-prompt adscmd.exe or adsvw.exe to bind specifically to a directory object.


Querying a Directory Service

Because many queries to a directory service behave like database queries and return tables of information, ADSI supports interfaces that allow you to define queries.

For both Automation clients using ActiveX Data Objects (ADO) and all non-Automation clients, ADSI supplies an OLE DB provider, supporting a subset of OLE DB query interfaces. Client code that already uses OLE DB interfaces for queries can use the same interfaces to query directory services.

For non-Automation clients, ADSI providers can support the IDirectorySearch interface on any ADSI object. This interface is a simpler implementation of the same OLE DB functionality. For information on whether or not this interface is supported by a system provider, see ADSI System Providers.

For more information, see Using Directory Service Queries.


ADSI Security Model

ADSI security interfaces support both authentication and access control.


About Authentication

You can supply credentials to a directory service using the IADsOpenDSObject::OpenDSObject method or the ADsOpenObject function. Both these routines include user-name and password parameters. A flags parameter can indicate binding option preferences when you want to use secure authentication or encryption options, but the directory service may override these flags. Because the flags are provider specific, the directory service may not support them at all. For example, the Microsoft® NDS-compliant ADSI provider always uses strong authentication. For more information, see ADSI System Providers.


About Access Control

The following interfaces support access control through access-control lists (ACLs): IADsSecurityDescriptor, IADsAccessControlList, and IADsAccessControlEntry interfaces supply the methods and properties necessary to represent a directory service's ACL lists. For more information, see Using Credentials and Security.


About Collections and Groups

ADSI uses collection objects to represent any arbitrary set of items in a directory service that can be represented using the same data type. Collection objects are defined as a set of VARIANT values, representing any of the valid Automation data types. Collection objects can represent both persistent information such as access-control lists and volatile information such as print jobs in a print queue.

The standard COM convention for listing the contents of a collection (or container) object is to create a enumerator object that supports IEnumVARIANT, which has methods to step through the list of collection objects. The interfaces in ADSI that supply the get__NewEnum method (note the two underscores) are IADsContainer, IADsMembers and IADsCollection. ADSI also supplies the helper functions ADsBuildEnumerator and ADsEnumerateNext to be used in C and C++ programs to simplify enumeration. Automation clients use enumeration implicitly when they call Next in a For loop.

Groups are simply collections of objects supporting the IADsMembers interface.

For examples, see the following:

  • Automation - Enumerating Users in a Group
  • COM Vtable Examples - Enumerating Users in a Group
  • Enumerating the ACLs on an ADSI Object — An Automation Example


About Provider-Specific Information

Each ADSI provider supplies implementations of Active Directory Service Interfaces in the most effective representation of the underlying directory service. The features and protocols of the native directory service do influence the ADSI implementations. For notes on each system implementation, see ADSI System Providers.


Using ADSI Objects and Interfaces

For ADSI client applications seeking to understand how all the elements of the ADSI object model fit together, this section provides plenty of examples.


IADs vs. IDirectoryObject Global Strategy

ADSI clients manage and manipulate directory service objects by using one of two COM interfaces: IADs or IDirectoryObject. IADs is an IDispatch interface intended for use by late-bound clients such as those written in Microsoft® Visual Basic®, Java, and various scripting languages. IDirectoryObject is a vtable interface that provides direct access to objects by early-bound clients such as those written in C and C++.

Each ADSI object must implement both IADs and IDirectoryObject. ADSI clients written in languages such as C or C++, which are able to directly access vtables, can use either interface but not both from one application. ADSI clients written in Visual Basic or Java are limited to using IADs.

The IADs interface enables late-bound clients to take advantage of the inherent housekeeping features of the ADSI object model. Among these features is the property cache, which enables clients to read and write properties without going over the wire for each call. In addition, client applications gain the use of powerful UI and ActiveX™ control libraries and a simpler style of programming. In return, late-bound clients must use the VARIANT data type, which precludes using the richer native data types provided by ADSI. For more information, see Using IADs.

The IDirectoryObject interface enables early-bound clients to take full advantage of native directory-service data types at the cost of foregoing a slight performance advantage from using the property cache. In return, the IDirectoryObject interface provides direct, on-the-wire access to object properties through a single request, rather than through individual get and put calls.. For more information, see Using IDirectoryObject.


Using IADs

In the ADSI environment, each element of a directory service is represented by an ADSI object, which is a Component Object Model (COM) object that supports the standard COM IUnknown interface as well as IDispatch and IADs. IADs supplies the basic house-keeping functions for ADSI objects.

The preceding figure shows a generic ADSI object supporting the fundamental interfaces IADs, IADsPropertyList, IUnknown, IDirectoryObject, and IDispatch. An ADSI object such as this manages data from the data store of the underlying directory service through the interfaces it supports. This data is known as the properties of the object, and the routines that retrieve and set these properties are known as property methods. Read-only properties have one property method that gets the value of the property. Read-write properties have two methods: one that sets the value and one that gets the value. Properties are implemented on each ADSI object using a property cache. IADs::get_ADsPath and IADs::put_ADsPath are examples of property methods. Property methods are not apparent to Visual Basic and other Automation clients that allow direct references to the property. For example, Visual Basic refers to IADS::ADsPath directly using the Object.ADsPath syntax. For more information, see Interface Property Methods.

In addition, an ADSI object interacts with other ADSI objects and directly to a namespace through methods. Methods execute immediately Examples of methods include IADs::SetInfo, IADs::GetInfo, and IADsPrintJobOperations::Pause.

Properties, property methods, and methods are all accessed through standard COM interfaces.

An ADSI object is uniquely identified by its ADsPath. An example of an ADsPath for the LDAP namespace is "LDAP://MyServer/DC=Seattle,DC=COM". For programmers familiar with COM monikers, this is conceptually similar to the COM moniker display name.

Any ADSI object that contains other ADSI objects (an ADSI container object) also supports the IADsContainer interface, which provides methods and properties that manage the creation, deletion, and enumeration of ADSI objects that are contained by the object. The following figure shows an ADSI container object.

Almost all ADSI objects are contained by other objects. The only ADSI object with no parent container is the top-level ADSI Namespaces object ("ADS:").

The IADs::SetInfo method on a container object persistently stores the cached properties of the ADSI container object to storage in addition to any objects that have been created with the IADsContainer::Create method. IADsContainer::Deletedoes not affect the property cache but deletes the underlying namespace directory element represented by this object immediately.


Using IDirectoryObject

Late-bound ADSI clients written in languages such as C and C++ can take advantage of the rich native data types used by ADSI by calling the IDirectoryObject interface instead of the IADs interface. The IDirectoryObject interface provides methods to support a subset of an object's housekeeping properties and to access its attributes. The following figure shows the relationships among the data structures:

In the preceding figure, the structure ADS_OBJECT_INFO defines properties that identify the object by distinguished name, relative distinguished name, by container (ParentDN), by object type (ClassDN), and by schema definition (SchemaDN). The attribute descriptor ADS_ATTR_INFO consists of a name, data type, an array of data values shown in ADSVALUE, and a flag that directs the underlying DS to perform certain operations on the attributes detailed in ADS_ATTR_* constants. The data types for these attributes include the ADSI extended syntax types, detailed in ADSTYPE.

Note Although IDirectoryObject provides most of the functionality of IADs, as well as new functionality of its own, it does not currently support ADSI container interfaces. Therefore, all ADSI objects must also implement IADs, which does provide such support.


Using the ADSI Schema

ADSI providers create a representation of the underlying schema by implementing the schema-management interfaces IADsClass, IADsProperty, and IADsSyntax. For example, on LDAP, IADsClass represents the attribute "objectClass" which contains values that identify every type of object known to an ldap server. NDS uses "object class". WinNT defines each object type directly, such as "Domain", "Computer", or "User". In all these cases, the IADsClass interface supplies methods to identify and manage the list of properties or attributes a class uses. Similarly, IADsProperty supplies methods to define a property as it is known to the underlying directory service. If you browse a directory service directly, you probably will not find anything corresponding to syntax objects. ADSI abstracts the syntax information as necessary and lets you use IADsSyntax to identify the syntax needed to represent an object's data.


ADSI Object Properties

The ADSI object model defines properties or attributes of an ADSI object for use in the housekeeping functions associated with directory services, all defined on the IADs interface. These properties include the fully qualified path name ADsPath, the unique identifier GUID, the Parent container object, the Schema object, and the Class or type of the object. These properties, along with the properties defined for this type of object that have also been defined in the underlying directory service, are cached in a property cache for each object.

The Class definition, found in the schema, specifies certain attributes that can be filled in from the underlying directory-service schema or namespace as available. The MandatoryProperties attribute supplies a list of property names that must be supplied before the object can be saved in persistent storage. The OptionalProperties list the properties that may be set for an object of this type.


Property Cache

The ADSI object model provides a client-side property cache for each ADSI object. An object's property cache enables client applications to set multiple properties in a single call. This capability is critical for clients written in languages, such as Microsoft Visual Basic, that have no native batching mechanism for setting properties. Without the ADSI property cache, such clients would have to hit the wire for each and every put call, with a predictable drop in performance.

Think of the cache as a table in memory that contains the names and values of all an object's properties. When an object is created, its property cache is empty. Calling IADs::GetInfo loads the object's properties from the underlying directory service. If a property is multi-valued, use the GetInfoEx method instead. If the cache is empty, calling get or get_propertymethod from a client written in C or C++, or calling .propertymethod from an Automation client, , forces an implicit GetInfo. Once the cache is filled, all get and put calls work on the contents of the cache only. You change the property values in the tables as necessary for your application using IADs methods or IADsPropertyList methods, then write the changes back out to the directory service when they are in a consistent state using SetInfo. After the SetInfo is called, the cache values remain until they are explicitly refreshed with a GetInfo. The following figure shows the different methods used to operate on the cache.

As shown in the preceding figure, while the property values are in the cache, you can access them in two ways:

Accessing an Individual Property in the Property Cache

Accessing All Properties in the Property Cache


Accessing an Individual Property in the Property Cache

Individual properties in the property cache can be referenced in several ways. From Automation languages, you use the property name directly. This is the ADSI property name, for example, Name or Class, both properties defined on IADs. You can also use the property names as it is defined in the schema object. Then you can use the IADs::Get or any of its related interfaces, GetEx, Put, and PutEx. In each of these cases, you pass the name of the property as it is defined in the schema as the first parameter.

From non-Automation languages, you can also use the IADs::Get method and its related methods as described previously. In addition, you can use the get_propertyname and put_propertyname methods supported on all ADSI interfaces.

To find the property names supported by an object, first get the IADs interface pointer for that object. Use IADs::Schema to find the schema object, and IADs::Class to look up the class definition. Using the IADsClass interface on the class definition, use property methods MandatoryProperties and OptionalProperties to see the list of properties defined for each object of this type.

Note In the following discussion, "get methods" include IADs::Get, IADs::GetEx, and any get_ property method defined on an ADSI interface. Similarly, "put methods" include IADs::Put, IADs::PutEx, and any put_ property method defined on an ADSI interface.

When an object is created, its property cache is empty. You can force all the properties to load by calling GetInfo. When you call one of the get methods and the cache is empty, an implicit GetInfo occurs. Once the cache is filled, all subsequent get and put requests are filled from the current contents of the cache, eliminating network calls. Only another explicit GetInfo refreshes the cache.

One consequence of this design occurs for the following scenario:

If the cache already has values, and you then set a previously undefined property in the underlying directory service, and you get that property, the cache will not have the value and the error return is E_ADS_PROPERTY_NOT_FOUND. You must explicitly call GetInfo to see the new value.

If you use a put method to change a value in the cache for a property and subsequently use a get method for that property, you retrieve the value you just put there. If you explicitly call GetInfo after you have changed values in the cache but before you call SetInfo, the values in the cache are refreshed from the underlying directory service and any changes you made are over-written.

SetInfo is always an explicit call, never called implicitly.

For more information, see IADs.

Examples of get and put methods for properties for Automation clients include the following:

MyObject.xxxx
Get by using object.property syntax on the right side of an assignment operation (=) or in an expression. Put by using object.property syntax on the left side of an assignment operator (=).
Value=(User.FullName)
Get by passing the name of the property. Use GetEx for multivalued properties.
MyObject.Put("Property_X", value)
Put by passing the name of the property. Use PutEx for multivalued properties.

The following Automation example shows both styles of setting the IADsUser::FullName property in the cache.

' Visual Basic example
Dim User as IADsUser
Dim MyObject as IADs
Dim Name as Variant

Set FullName as "John Q. Public"
Set User = GetObject("WinNT://MSFT/Users/John")

//Set using IADsUser::put_Fullname property method
User.FullName = Name

//Set using IADs::PutMethod
User.Put("FullName",Name)

Examples of get methods for properties for non-Automation clients include the following:

IADsXXXX::get_xxxx
An interface's compile-time property method beginning with get_ on a named property.
IADs::Get("Property_X")
A run time get by property name.
// For non-Automation clients
IADsUser     *pUser;
IADs         *PObject;
ADsGetObject(
    TEXT("WinNT://MSFT/Users/John"),
    IID_IADsUser,
    (void**) &pUser);
// Set using IADsUser::put_FullName property method
pUser->put_FullName(TEXT("John Q. Public"));
// Set using IADs::Put method
pUser->QueryInterface(IID_IADs,(void **) &pObject);
pObject->Put("FullName",TEXT("John Q. Public"))


Accessing All Properties in the Property Cache

In addition to the IADs methods that get and set individual properties in the cache, ADSI provides the IADsPropertyList, IADsPropertyEntry, and IADsPropertyValue interfaces that allow client software to access the property cache directly. These interfaces allow you to work with the properties without having to use the individual property names.

ADSI clients call:

Interface | To:
IADsPropertyList | Manage the entire list of properties defined on an object.
IADsPropertyEntry | Manage individual property attributes and values.
IADsPropertyValue | Read and write values for each property entry in the property cache.

The following figure shows the property cache as a table.

In the preceding figure, each entry in the cache contains the property name, the number of values that exist for the property, the data type needed to represent its information, and the actual value list. The ADSI property-cache interfaces represent each property entry. IADsPropertyEntry allows access to a single property in the cache and includes a pointer to the list of IADsPropertyValue pointers needed to represent each value. IADsPropertyList allows you to manage the property entries, including enumerating all the properties in the cache.


Standard Syntax Objects

An ADSI schema container object contains the syntax objects necessary to represent all the property-object data in all the schema classes. Syntax objects by definition are Automation-compliant. Extended syntax support for non-Automation clients is defined in ADSTYPE.

Syntax Name | IDL Type Representation | Automation VARTYPE | Remarks
AccessPointDN | BSTR | VT_BSTR | Distinguished Name (used internally by DS)
ADsPath | BSTR | VT_BSTR | String that uniquely identifies an object.
Boolean | VARIANT_BOOL | VT_BOOL | TRUE = -1, FALSE = 0;
Counter | LONG | VT_I4 | Differs from Integer in that any value added is added to the total, and any value deleted is deleted from the total.
DN | BSTR | VT_BSTR | Distinguished Name
EmailAddress | BSTR | VT_BSTR |
FaxNumber | BSTR | VT_BSTR | Should comply with E.123, with an optional bit string formatted according to T.30.
IA5String | BSTR | VT_BSTR | String in the IA5 character set.
Integer | LONG | VT_I4 |
Interface Ptr | IDispatch | VT_DISPATCH | Identifies a function available on this COM object.
Interval | LONG | VT_I4 | Value represents an interval of time, in seconds. -1 = Unlimited.
Large Integer | VARIANT | VT_VARIANT | An object
List | VARIANT | VT_VARIANT | Value is an ordered sequence of strings. The VARIANT contains a SAFEARRAY of BSTRs.
NetAddress | BSTR | VT_BSTR | A network address, represented as a string.
NumericString | BSTR | VT_BSTR |
OctetString | VARIANT | VT_VARIANT | Values are uninterpreted byte strings. The VARIANT contains a SAFEARRAY of unsigned chars.
OID | BSTR | VT_BSTR | Object Identifier in dotted decimal form (1.2.3.4.5 and so on)
ORName | BSTR | VT_BSTR | X.400 Originator/Recipient name.
Path | BSTR | VT_BSTR | Value is a file system path.
PhoneNumber | BSTR | VT_BSTR |
PostalAddress | BSTR | VT_BSTR | Value is a postal address, typically 6 lines of 30 characters each. Carriage return characters are allowed. The following format is recommended: Line 1: Name Line 2: Street Address or PO Box Number Line 3: State or Province Name Line 4: Postal Code Line 5: Country
SmallInterval | LONG | VT_I4 | Value represents an interval of time in milliseconds.
String | BSTR | VT_BSTR |
Time | DATE | VT_DATE |
UTCTIME | BSTR | VT_BSTR | Time string in Universal Coordinated Time format.


Using Credentials and Security

ADSI provides interfaces to enforce both user authentication and access control on directory service objects.


Authentication

In ADSI, credentials consisting of a user name and password allow system software to authenticate the identity of a user during the initial system login. Use IADsOpenDSObject::OpenDSObject or the helper function ADsOpenObject to establish a user's credentials for a directory service. If you don't use OpenDSObject, ADSI uses default credentials. Once the system authenticates the user, the user is free to make requests for system resources, access to file shares, and file permissions, as supported by the underlying directory service's security requirements.

The following examples show the use of the OpenDSobject method on different directory services:

Dim MyNamespace As IADsOpenDSObject

Set MyNamespace = GetObject("LDAP:")
Set X = MyNamespace.OpenDSObject(DN, UserName, Password, Flags)


Access Control

Privileges to access directory service resources are usually granted through the use of access control entries (ACEs). An ACE defines an access or audit permission on an object for a specific user or group. An access-control list (ACL) is the ordered collection of ACEs defined for an object. A security descriptor supports properties and methods that create and manage ACLs. For more information on security models, see the "Windows Base Services" section of the Microsoft Platform SDK or Microsoft Windows 2000 Resource Kit.

ADSI clients call:

Interface | To:
IADsAccessControlEntry | Read and write ACE properties.
IADsAccessControlList | Manage and enumerate all ACEs on a directory service object.
IADsSecurityDescriptor | Read and write security properties of a directory service object.

The following figure shows the traditional finance department example: the owner of a confidential document has the complete set of permissions for accessing it. The temporary help has no access to the document object. The other members of the finance department have read-only access.

The preceding figure shows that a property or attribute called "ntSecurityDescriptor" has been created for a generic directory service object. As this is a generic object that does not support the IADs interface, the ADsPath property has also been created for this object and its value is "LDAP://o=ms/ou=printers/cn=FancyExpensiveColorPrinter". The value of the property for "ntSecurityDescriptor" is a pointer to an interface on a security descriptor object that, among other properties, has a property named "DiscretionaryACL". This property contains a pointer to an interface on an AccessControlList object that supplies a method to create an enumerator object that in turn allows you to list the individual AccessControlEntry objects defined on the original document object.

Most security checking algorithms have rules about the ordering of different types of ACEs. Since ADSI packs the ACEs in the order specified by the client to be sent to the directory service and preserves the order when loading them from the directory service, the ordering is preserved.


Creating a Security Descriptor Object

The following Visual Basic example shows how to create a security descriptor object for an ADSI object.

' Assume you have the credentials to create objects
Dim MyObject As IADs
Dim MySecDes As IADsSecurityDescriptor
Dim Var As Variant
Dim SecDes As New SecurityDescriptor
Dim Dacl As New AccessControlList
Dim Ace As New AccessControlEntry

'Create an Access Control Entry (ACE) for Group objects at Microsoft
'    on an LDAP namespace.
Ace.AccessMask = 0
Ace.AceType = 1
Ace.AceFlags = 1
Ace.Trustee = "cn=Groups,o=Microsoft"

' Add the newly created ACE object as the only ACE
'    in a new access-control list (ACL)
Dacl.AceCount = 1
Dacl.AclRevision = 4
Dacl.AddAce Ace

' Use this ACL as the discretionary ACL (DACL) for
'    this Security Descriptor object and use
'    this DACL instead of the default.
SecDes.Revision = 1
SecDes.OwnerDefaulted = True
SecDes.GroupDefaulted = True
SecDes.DaclDefaulted = False
SecDes.SaclDefaulted = True
SecDes.DiscretionaryAcl = Dacl
' Attach  this security descriptor
'    to the ADSI object
MyObject.Put "ntSecurityDescriptor", SecDes
' Commit the changes to the underlying DS
MyObject.SetInfo
' Read the properties back in to the property cache
MyObject.GetInfo
' Retrieve the SecurityDescriptor object
Set MySecDes = MyObject.Get("ntSecurityDescriptor")


Enumerating the ACLs on an ADSI Object

The following Visual Basic example shows how to enumerate the discretionary ACL on a security descriptor object.

Dim X as IADs
Dim Namespace As IADsOpenDSObject
Dim SecurityDescriptor As IADsSecurityDescriptor
Dim Dacl As IADsAccessControlList

' First get access to the LDAP directory service
Set Namespace = GetObject("LDAP:")
' Establish your credentials with no password
Set X = Namespace.OpenDSObject("LDAP://MyLdapSvr/O=Internet/DC=MS",
    "cn=administrator,DC=MS,O=Internet", "", 1)
' Retrieve the contents of the ntSecurityDescriptor field which is
'    an interface pointer on the security descriptor object
'    for the X object
Set SecurityDescriptor = X.Get("ntSecurityDescriptor")
' Print out the owner of the object
' Print out the group
Debug.Print SecurityDescriptor.Owner
Debug.Print SecurityDescriptor.Group
'
Set Dacl = SecurityDescriptor.DiscretionaryAcl
' Enumerate the ACEs in the Dacl, first printing out the count
Debug.Print Dacl.AceCount

For Each Obj In Dacl
    Debug.Print Obj.Trustee
    Debug.Print Obj.AccessMask
    Debug.Print Obj.AceFlags
    Debug.Print Obj.AceType
Next


Using Directory Service Queries

Querying a directory service using ADSI is similar to querying a database. Because the potential amount of information to be searched could be millions of data elements (or more), a good query strategy allows you to limit your search in various ways. ADSI uses the following concepts in defining a search:

Base DN
The root node of a search. The base DN (distinguished name) must be a fully qualified name in the form: "WinNT://MyDomain/MyServer/User" or "WinNT://MyDomain/MyServer/MyPrintQueue". (Distinguished names are not case sensitive. The use of capital letters in the preceding examples is for clarity only.)
Filter
A string that specifies the criteria to be met by the objects to be returned in a query. This string is in LDAP search filter format, as described in RFC 1960. More information about this can be found in the document ftp://ds.internic.net/rfc/rfc1960.txt. For more information on using a search filter, see Using a Search Filter.
Attribute or Property List
Identifying those attributes of interest allows the search to ignore all other attributes of the object.
Scope
In a directory hierarchy, you need to choose the beginning entry of the search and decide if you want to search recursively through all branches of that node. Scope is one of the values "Base", "OneLevel", or "SubTree", which are defined in ADS_SCOPEENUM.
Preference
Search preference includes other ways to limit a query. A search preference set allows you to indicate limits such as the size of the query result, time limits, whether or not to dereference aliases during the search, whether to use secure authentication and other limits.

To use ADSI to query a directory service, you can use IDirectorySearch or the ADSI OLE DB provider. For more information, see Using the ADSI OLE DB Provider for Searches or Using IDirectorySearch for Querying.


Using a Search Filter

The search filter specifies the relationship between the attributes and values of those attributes in the prefix expression notation.

The following are examples of valid search filters:

Search Filter | Meaning
(objectClass=*) | All objects that have objectClass attribute present.
(cn=FooBar) | All objects having cn property equal to "FooBar".
(&(objectClass=User)(revision>=10)) | All user objects that have the property revision greater than or equal to 10.
(&(objectClass=Computer)(|(name=a*)(name=b*))) | All Computer objects which have name attribute starting with "a" or "b".

The formal definition of the search filter is as follows (from RFC 1960):

<filter> ::= '(' <filtercomp> ')'
<filtercomp> ::= <and> | <or> | <not> | <item>
<and> ::= '&' <filterlist>
<or> ::= '|' <filterlist>
<not> ::= '!' <filter>
<filterlist> ::= <filter> | <filter> <filterlist>
<item> ::= <simple> | <present> | <substring>
<simple> ::= <attr> <filtertype> <value>
<filtertype> ::= <equal> | <approx> | <ge> | <le>
<equal> ::= '='
<approx> ::= '~='
<ge> ::= '>='
<le> ::= '<='
<present> ::= <attr> '=*'
<substring> ::= <attr> '=' <initial> <any> <final>
<initial> ::= NULL | <value>
<any> ::= '*' <starval>
<starval> ::= NULL | <value> '*' <starval>
<final> ::= NULL | <value>

The token <attr> is a string representing an AttributeType. The token <value> is a string representing an AttributeValue whose format is defined by the underlying directory service.

If a <value> must contain one of the characters * or ( or ), the character should be preceded by \ escape character.


Using the ADSI OLE DB Provider for Searches

Under the OLE DB implementation, a directory service is exposed as a Data Source Object. Data source objects are factories for session objects and support IDBInitialize to connect to the directory, IDBCreateSession to create the session object, IDBProperties to supply authentication information to the underlying namespace and supply the query command and IPersist to save the information necessary to create the data source object to the underlying directory service.

The OLE DB Sessions Object represents a transaction on the data source object and, in the ADSI implementation, can create rowset objects and command objects. It supports IOpenRowSet to allow direct access to DS tables without having to use command objects. ISessionProperties to manage the information stored in property sets, and IDBCreateCommand to create a command object.

The OLE DB Command Object manages the information required by a search. The object supports the following interfaces:

  • ICommand methods execute the command once it is set up.
  • IAccessor methods describe how the data should be stored in a consumer's buffer.
  • IColumnsInfo methods provide information about the columns.
  • ICommandProperties methods to specify some of the characteristics of the search, for example, Scope and Deref Aliases.
  • ICommandText methods to set the command text, which contains the basic search information including the root of the search, search filter, and list of attributes to be returned.
  • IConvertType to examine what kind of data conversions among different data types are allowed by the provider.

The OLE DB Rowset Object allows the DS to expose its information in tabular form. The ADSI implementation of this object supports the following interfaces:

  • IRowset methods traverse the tables row by row.
  • IAccessor methods describe how the data should be stored in a consumer's buffer.
  • IColumnsInfo methods provide information about the columns.
  • IConvertType methods determine if the type of information for this row can be converted to, for example, an Automation-supported type.
  • IRowsetLocate, IRowsetScroll, and IRowsetIndex methods support complex rowset navigational methods that include cursoring, bookmarks, and client-side filtering.

For more information about OLE DB, see the Database and Messaging Services section of the Microsoft Platform SDK.

To see if a specific provider implements these interfaces, see ADSI System Providers.

The ADSI SDK includes adsqry source code in the sampapp\cxx\adsqry directory. For an edited version of this example, see ADSI OLE DB Example.


ADSI SQL Grammar

The following are examples of ADSI queries using SQL:

Select ADsPath, Cn FROM 'LDAP://O=Internet/DC=COM/DC=MICROSOFT/DC=NTDEV' where objectClass='group' :  Select all group objects

Select AdsPath, cn, revision FROM
'LDAP://nw01t1/O=Internet/DC=COM/DC=MICROSOFT/DC=NTDEV/dc=nwparent' where objectClass='*' AND revision >= 10 : All user objects which have the property revision greater than or equal to 10

Select AdsPath, cn, revision FROM 'LDAP://nw01t1/O=Internet/DC=COM/DC=MICROSOFT/DC=NTDEV/dc=nwparent' where objectClass='*' AND (cn = 'a*' OR cn = 'b*'):  All Computer objects which have name attribute starting with "a" or "b"

The formal grammar for the SQL queries is defined in the following way. (All keywords are case insensitive.)

statement ::= select-statement

select-statement ::= SELECT [ALL] select-list FROM table-identifier [WHERE search-condition]

select-list ::= * | select-sublist [, select-sublist]...

select-sublist ::= column-identifier

column-identifier ::= user-defined-name

table-identifier ::= string-literal

search-condition ::= boolean-term [OR search-condition]

boolean-term ::= boolean-factor [AND boolean-term]

boolean-factor ::= [NOT] boolean-primary

boolean-primary ::= comparison-predicate | (search-condition)

comparison-predicate ::= column-identifier comparison-operator literal

comparison-operator ::= < | > | <= | >= | = | <>

user-defined-name ::= letter [letter | digit]...

literal ::= string-literal | numeric-literal | boolean-literal

string-literal ::= '{character}...' (Any sequence of characters delimited by quotes)

numeric-literal ::= digits [fraction] [exponent]

digits ::= digit [digit]...

fraction ::= . digits

exponent ::= E digits

boolean-literal ::= TRUE | FALSE | YES | NO | ON | OFF


ADSI OLE DB Example

This following ADSI example shows a simple command-prompt program that uses the OLE DB interfaces to query a directory service. The beginning comment section describes which functions were removed to simplify the example. The complete example code is included in the ADSI SDK in the samples/activeds/sampapp/cxx/adsqry directory.

// ... Global declarations, free memory calls, and error handling
//        Print routines, mapping input arguments to
//        search arguments and so on are omitted for brevity.

#include "main.hxx"

int __cdecl
main( int argc, char ** argv)
{
 //  Load up a DBPropSet for DB data
 //    and a CmdPropSet for Command data
    hr = ProcessArgs(argc, argv);
    hr = CoInitialize(NULL);
    //
    // Instantiate a data source object for LDAP provider
    //
    hr = CoCreateInstance(
        CLSID_ADsDSOObject,
            0,
            CLSCTX_INPROC_SERVER,
            IID_IDBInitialize,
            (void **)&pIDBInit
            );
 // Initialize the Data Source
    hr = pIDBInit->Initialize();
    if (cDBPropSet) {
        pIDBInit->QueryInterface(
            IID_IDBProperties,
            (void**) &pIDBProperties);
        hr = pIDBProperties->SetProperties(
                 cDBPropSet,
                 rgDBPropSet);

    }
 // Get the interface to create a session
    pIDBInit->QueryInterface(
        IID_IDBCreateSession,
        (void**) &pIDBCS);

 // Create a session returning a pointer to its CreateCommand interface
    hr = pIDBCS->CreateSession(
            NULL,
            IID_IDBCreateCommand,
            (LPUNKNOWN*) &pIDBCreateCommand
            );
 // Create a command from the session object
    hr = pIDBCreateCommand->CreateCommand(
            NULL,
            IID_ICommandText,
            (LPUNKNOWN*) &pICommandText
            );
 // Set the CommandText for the Query
    hr = pICommandText->SetCommandText(
            rguidDialect,
            pszCommandText
            );
// Set the properties on the Command Object
    if (cCmdPropSet) {
        hr = pICommandText->QueryInterface(
                IID_ICommandProperties,
                (void**) &pICommandProperties);
        hr = pICommandProperties->SetProperties(
                cCmdPropSet,
                rgCmdPropSet);
    }

    hr = pICommandText->QueryInterface(
            IID_ICommand,
            (void**) &pICommand);

 // Do the Query and get back a rowset
    hr = pICommand->Execute(
            NULL,
            IID_IRowset,
            NULL,
            NULL,
            (LPUNKNOWN *)&pIRowset
            );
    hr = pIRowset->QueryInterface(
            IID_IColumnsInfo,
            (void**) &pIColsInfo
            );
    hr = pIColsInfo->GetColumnInfo(
            &cCol,
            &prgColInfo,
            &szColNames
            );
 // The no. of attributes is one less than the no. of columns because of
 //  the Bookmark column
    nAttrs = cCol - 1;
    pMyStatus = (DBBINDSTATUS *) LocalAlloc(
                                     LPTR,
                                     sizeof(DBBINDSTATUS) * nAttrs
                                     );
    hr = CreateAccessorHelper(
            pIRowset,
            nAttrs,
            prgColInfo,
            &myAccessor,
            pMyStatus
            );
    pMyData = (Data *) LocalAlloc(
                           LPTR,
                           sizeof(Data) * nAttrs
                           );
    //  Get the rows; 256 at a time
    phRows = NULL;
    hr = pIRowset->GetNextRows(
            NULL,
            0,
            256,
            &cRowsObtained,
            &phRows
            );
    j = cRowsObtained;
    while (cRowsObtained) {
        for (i = 0; i < cRowsObtained; i++) {
 // Get the data from each row
            hr = pIRowset->GetData(
                    phRows[i],
                    myAccessor,
                    (void*)pMyData
                    );
            PrintData(pMyData, nAttrs, prgColInfo);
        }
        pIRowset->ReleaseRows(
                        cRowsObtained,
                        phRows,
                        NULL,
                        NULL,
                        NULL
                        );
 // Get the next 256 rows
        hr = pIRowset->GetNextRows(
                NULL,
                0,
                256,
                &cRowsObtained,
                &phRows
                );
        j+=cRowsObtained;
    }
    printf("Rows printed = %d\n", j);
    exit(0);

}


Using ADO for Querying

The following example shows how Automation clients can use ActiveX Data Objects (ADO) to query directory services using ADSI and its OLE DB implementation. The ADO object model consists of the following objects:

Connection
An open connection to an OLE DB data source such as ADSI.
Command
Defines a specific command to execute against the data source.
Parameters
An optional collection for any parameters to provide to the command object.
Recordset
A set of records from a table, command object, or SQL syntax. A recordset can be created without any underlying Connection object.
Field
A single column of data in a recordset.
Property
A collection of values supplied by the provider for ADO.
Error
Contains details about data access errors, refreshed for each time an error occurs in a single operation.

For more information, see the Microsoft Platform SDK under ActiveX Data Objects.

The following example disables the user accounts on an ldap server.

Dim X as IADs
Dim con As New Connection, rs As New Recordset
Dim MyUser As IADsUser

con.Provider = "ADsDSOObject"
con.Open "Active Directory Provider", "CN=Foobar,CN=Users,DC=MICROSOFT,DC=COM,O=INTERNET", "Password"
Set rs = con.Execute("<LDAP://ldapserver.microsoft.com/O=Internet/DC=COM/DC=MICROSOFT/OU=MyOrg>;(objectClass=User);ADsPath;onelevel")

While Not rs.EOF
    MyUser = GetObject(rs.Fields(0).Value)
    MyUser.AccountDisabled = True
    rs.MoveNext
Wend


Using IDirectorySearch for Querying

In addition to the search capabilities supported by the ADSI OLE DB provider, ADSI supplies the IDirectorySearch interface to query directly directory services for non-Automation clients. IDirectorySearch requires almost the same search information as the methods of the OLE DB interfaces, but reduces the programming overhead. Use IDirectorySearch when you just want to specify a query and get the results back without any of the richer navigation features supplied when using OLE DB queries.

When implemented by a provider, IDirectorySearch is supported on every ADSI object. For information on which providers support this interface, see ADSI System Providers.

Use IDirectorySearch::SetSearchPreference to set the preferred settings for your query. Once your preferences are created, use ExecuteSearch to initiate the query. Query results are exposed in the form of a table. Each row is one entry of the result. Each column within the row represents an attribute or property. The following figure shows a simplified representation of how the query process works.

Use GetNextRow to advance the search handle shown in the preceding figure row by row, and GetNextColumnName with GetColumn to retrieve results from the columns. With each row you step through the columns of interest looking for the attributes that match. This process continues until there are no more rowsets in the query results.

The following ADSI example shows a simple command-prompt program that uses the IDirectorySearch interface to query a directory service. This sample has been edited, primarily by removing error checking, to simplify the flow. The complete example code is included in the ADSI SDK in the samples/activeds/sampapp/cxx/dssrch directory.

Usage: dssrch /b <baseObject> /f <search_filter> [/f <attrlist>] [/p <preference>=value>]  [/u <UserName> <Password>] [/t <flagName>=<value>

    where:
    baseObject     = ADsPath of the base of the search
    search_filter  = search filter string in LDAP format
    attrlist       = list of the attributes to display
    preference could be one of:
    Asynchronous, AttrTypesOnly, DerefAliases, SizeLimit, TimeLimit,
    TimeOut, PageSize, SearchScope
    flagName could be one of:
    SecureAuth or UseEncrypt
    value is yes/no for a Boolean and the respective integer for integers
    scope is one of "Base", "OneLevel", or "Subtree"

For Example: dssrch /b NDS://ntmarst/ms /f "(object Class=Group)"  /a  "ADsPath, name, description" /p searchScope=onelevel

#define INC_OLE2
#include <windows.h>
#include <stdlib.h>
#include <limits.h>
#include <io.h>
#include <stdio.h>
#define BAIL_ON_NULL(p) if (!(p)) {goto error;}
#define BAIL_ON_FAILURE(hr) if (FAILED(hr)) {goto error;}

#include "activeds.h"
#include "main.hxx"
// Globals representing parameters on IDirectorySearch
LPWSTR pszSearchBase, pszSearchFilter, pszAttrNames[10], pszAttrList;
DWORD dwNumberAttributes = -1;
// Preference Values
BOOL fASynchronous=FALSE, fDerefAliases=FALSE, fAttrsOnly=FALSE;
DWORD fSizeLimit, fTimeLimit, dwTimeOut, dwPageSize, dwSearchScope;

ADS_SEARCHPREF_INFO pSearchPref[10];
DWORD dwCurrPref = 0;

LPWSTR pszUserName=NULL, pszPassword=NULL;
DWORD dwAuthFlags=0;
DWORD cErr=0;

char *prefNameLookup[] =
    {
    "ADS_SEARCHPREF_ASYNCHRONOUS",
    "ADS_SEARCHPREF_DEREF_ALIASES",
    "ADS_SEARCHPREF_SIZE_LIMIT",
    "ADS_SEARCHPREF_TIME_LIMIT",
    "ADS_SEARCHPREF_ATTRIBTYPES_ONLY",
    "ADS_SEARCHPREF_SEARCH_SCOPE",
    "ADS_SEARCHPREF_TIMEOUT",
    "ADS_SEARCHPREF_PAGESIZE",
    "ADS_SEARCHPREF_PAGED_TIME_LIMIT",
    "ADS_SEARCHPREF_CHASE_REFERRALS"
    };

//+-----------------------------------------------------------------
//  Function:   main
//------------------------------------------------------------------
INT _CRTAPI1
main(int argc, char * argv[])
{

    hr = CoInitialize(NULL);
    hr = ADsOpenObject(
        pszSearchBase,
        pszUserName,
        pszPassword,
        dwAuthFlags,
        IID_IDirectorySearch,
        (void **)&pDSSearch
        );

    if (dwCurrPref) {
        hr = pDSSearch->SetSearchPreference(
                pSearchPref,
                dwCurrPref
                );
        BAIL_ON_FAILURE(hr);
        }

    hr = pDSSearch->ExecuteSearch(
            pszSearchFilter,
            pszAttrNames,
            dwNumberAttributes,
            &hSearchHandle
            );    BAIL_ON_FAILURE(hr);

    hr = pDSSearch->GetNextRow(
            hSearchHandle
            );    BAIL_ON_FAILURE(hr);

    while (hr != S_ADS_NOMORE_ROWS) {
        nRows++;

        if (dwNumberAttributes == -1) {
            hr = pDSSearch->GetNextColumnName(
                    hSearchHandle,
                    &pszColumnName
                    );             BAIL_ON_FAILURE(hr);

            while (hr != S_ADS_NOMORE_COLUMNS) {
                hr = pDSSearch->GetColumn(
                        hSearchHandle,
                        pszColumnName,
                        &Column
                        );

                if (FAILED(hr)  && hr != E_ADS_COLUMN_NOT_SET)
                    goto error;

                if (SUCCEEDED(hr)) {
                    PrintColumn(&Column, pszColumnName);
                    pDSSearch->FreeColumn(&Column);
                }

            if (pszColumnName)
            free(pszColumnName);
                hr = pDSSearch->GetNextColumnName(
                        hSearchHandle,
                        &pszColumnName
                        );    BAIL_ON_FAILURE(hr);
            }
            printf("\n");
        }
        else {
            for (unsigned int i=0; i<dwNumberAttributes; i++) {
                hr = pDSSearch->GetColumn(
                        hSearchHandle,
                        pszAttrNames[i],
                        &Column
                        );

                if (hr == E_ADS_COLUMN_NOT_SET)
                    continue;

                BAIL_ON_FAILURE(hr);

                PrintColumn(&Column, pszAttrNames[i]);

                pDSSearch->FreeColumn(&Column);
            }
        printf("\n");
        }

        hr = pDSSearch->GetNextRow(
                hSearchHandle
                );
        BAIL_ON_FAILURE(hr);
    }

    wprintf (L"Total Rows: %d\n", nRows);

    if (cErr) {
        wprintf (L"%d warning(s) ignored", cErr);
    }

    if (hSearchHandle)
        pDSSearch->CloseSearchHandle(hSearchHandle);

    FREE_INTERFACE(pDSSearch);

    FREE_UNICODE_STRING(pszSearchBase) ;
    FREE_UNICODE_STRING(pszSearchFilter) ;
    FREE_UNICODE_STRING(pszAttrList) ;

    CoUninitialize();

    return(0) ;

error:
//  Error handling
}


Debugging Hints

Here are some hints for the most common errors experienced by first-time ADSI application writers:

Case-sensitivity
The ProgIDs for ADSI are case sensitive! This is a feature of the underlying namespace because some directory services use case sensitive path names. For example, when you call ADsGetObject with an ADsPath name for the ADSI LDAP provider, use "LDAP:" and not "ldap:".
VT_BOOLEAN for Automation clients
A reminder that VT_BOOLEAN is defined as -1, not 1.
Dim for Automation clients
The Dim() statement sets the upper bound of an array. For example, if you Dim(8), you get an array of size 9 where the first element index is 0 and the last element index is 8. If you initialize just 8 elements of the array and pass the result to an ADSI method expecting an initialized VARIANT, Visual Basic errors out.
Binding to an object over the net
If you are having trouble getting ADsGetObject to get past the server connection and return without error, try accessing the same ADsPath string through a command prompt window, using the "net use <ADsPath>".For example, if
GetObject("WinNT://MyDomain/MyComputer")

results in an error, enter the following at the command prompt:

>net use * \\MyDomain\netlogon

If you can successfully execute the command prompt syntax, you know your ADsPath is accurate and that your network connections are valid and can continue to look elsewhere for the problem.

Not finding properties for an object
In some implementations of ADSI, there is a direct correspondence between the properties listed in a schema definition for an object and the underlying properties in a directory service. In other implementations of ADSI, the property names included in the mandatory and optional attribute lists of the schema class object may be simulated from various places in the underlying system to supply an easy way to work with these properties. In both cases, when client software calls .get or get_propertyname and a corresponding property does not exist in the underlying directory service, E_ADS_PROPERTY_NOT_FOUND is returned. Whether the underlying directory service property had never been set or there is no underlying value, the client code should handle the result gracefully. For more information about how the property cache works, see Property Cache.


Automation Examples

This section contains short code examples in Visual Basic® for ADSI client applications.

Automation - Retrieving a Property From an ADSI Object

Automation - Browsing a Schema Class Definition

Automation - Adding Users to a Group

Automation - Removing Users From a Group

Automation - Listing Children in a Container

Automation - Enumerating Users in a Group

Automation - Creating a User Object

Automation - Deleting a User Object

Automation - Using a Filter


Automation - Retrieving a Property From an ADSI object

The following example retrieves a property directly from an ADSI object using the standard IADsUser interface and the Fullname property it supports.

Dim MyUser as IADsUser
Dim MyName as String

Set MyUser = GetObject("NDS://O=MyOrg/OU=SomeOU/CN=JamesSmith")
MyName = MyUser.Fullname

The following example retrieves a property by name from an object using IADs::Get. If the property is multivalued, IADs::GetEx must be used. Note that because IADsUser inherits from IADs, the methods supported by IADs are automatically accessible.

Dim MyUser as IADsUser
Dim MyName as String

set MyUser = GetObject("NDS://O=MyOrg/OU=SomeOU/CN=JamesSmith")
MyName = MyUser.Get("Fullname")

The following example uses IADsPropertyList using the Next method to retrieve all the properties on a given object from an LDAP server.

Dim MyVar As Variant
Dim MyADsPropList As IADsPropertyList
Dim MyADsObj As IADsOpenDSObject
Dim DN As String
Dim UserName As String
Dim Password As String
Dim propcount As Integer

'Bind to specific object
DN = "LDAP://MyServer/dc=Mydomain,dc=microsoft,dc=com,o=internet"
UserName = "cn=Administrator,cn=Users,dc=Mydomain,dc=microsoft,dc=com,o=internet"
Password = "password"
Set MyADsObj = GetObject("LDAP:")
Set MyADsPropList = MyADsObj.OpenDSObject(DN, UserName, Password, 0)

' Get the properties into the cache !
MyADsPropList.GetInfo

'Get the Number of Properties in the list and walk the list
propcount = MyADsPropList.PropertyCount

Debug.Print "There are ", propcount, " properties."

For i = 1 To propcount
    Set MyVar = MyADsPropList.Next
    Debug.Print "The property Name from Variant is "; MyVar.Name
Next

On Error Resume Next
' This call will fail !
Set MyVar = MyADsPropList.Next
If Err.Number Then
    Debug.Print Err.Number; " Occurred "
    Err.Clear
End If

' Now Reset the property Index and then call next
' This will return the first value
MyADsPropList.Reset
Set MyVar = MyADsPropList.Next
Debug.Print "The first element in the list has the name ", MyVar.Name


Automation - Browsing a Schema Class Definition

This example uses the WinNT provider schema.

    Dim Computer As IADsComputer
    Dim Class As IADsClass
    Dim Property As IADsProperty
    Dim V As Variant
    Dim I As Integer

    Set c = GetObject("WinNT://MyDomain/MyMachine,Computer")
    Set Class = GetObject(c.Schema)

    Debug.Print "Class: " & Class.Name
    Debug.Print "GUID: " & Class.GUID
    Debug.Print "Implemented by: " & Class.CLSID

    If Class.Container Then
        Debug.Print "Container Object"
        Debug.Print "Class Contains:"
        For Each V In Class.Containment
            Debug.Print "   "; V
        Next V
    Else
        Debug.Print "Leaf Object"
    End If

    Debug.Print "Properties in this Class: "
    For Each V In Class.MandatoryProperties
        Debug.Print "   "; V
    Next V
    For Each V In Class.OptionalProperties
        Debug.Print "   "; V
    Next V


Automation - Adding Users to a Group

Dim Group as IADsGroup

' Bind to a known group object.
Set Group = GetObject("WinNT://MyDomain/Groups/Administrators")

' Add Jane and John to the Administrators group.
Group.Add("WinNT://MyDomain/Users/John")
Group.Add("WinNT://MyDomain/Users/Jane")


Automation - Removing Users From a Group

Dim Group as IADsGroup

' Bind to a known group object.
Set Group = GetObject("WinNT://MyDomain/Groups/Administrators")

' Remove Jane and John from the Administrators group.
Group.Remove("WinNT://MyDomain/Users/John")
Group.Remove("WinNT://MyDomain/Users/Jane")


Automation - Enumerating Users in a Group

Dim Group as IADsGroup
Dim Member as IADs

' Bind to a known group object.
Set Group = GetObject("WinNT://MyDomain/Administrators")

' Enumerate the members in the group.
For Each Member in Group.Members
    Debug.Print Member.Name
Next Member


Automation - Creating a User Object

Dim Container as IADsContainer
Dim NewUser as IADsUser

' Bind to the known container.
Set Container = GetObject("WinNT://MyDomain")

' Create the new wrapper.
Set NewUser = Container.Create("user", "Jane")
' Write it back to the DS
NewUser.SetInfo

' Set Jane's password.
NewUser.SetPassword("PW.1234")


Automation - Listing Children in a Container

Dim MyObject as IADs
Dim Child as IADs
Dim Container as IADsContainer

On Error Resume Next
Set MyObject = GetObject("WinNT://MyDomain/Group/Admin")
Set Container = MyObject

If Err = 0 Then
    For Each Child in Container
        Debug.Print Child.Name
    Next Child
EndIf


Automation - Deleting a User Object

Dim Container as IADsContainer
Dim User as IADsUser
Dim UserPath as String

' Bind to the known object.  In this case, the explicit path is known,
' but typically, the object will be discovered
' using query or enumeration.
Set User = GetObject("WinNT://MyDomain/Users/Jane")

' Get the container of the user object.
Set Container = GetObject(User.Parent)

' Delete the user.
Call Container.Delete("user",User.name)

' Release the user object.  Note that
' the underlying DS object was deleted, so do not hang on to the user
' component.
Set User = Nothing


Automation - Using a Filter

The Example is TBD.


Automation Examples Using Java

This section contains short code samples that show simple ADSI operations like binding and object creation and deletion. The expanded code samples that include complete Java code are included in the SDK samples directory.

A Java implementation of ADSI is a separate product found under JavaADSI and is part of the Application Foundation Classes (AFC).


Java - Binding to an ADSI Object

// bind.java - Edited simple demonstration of binding
//    a DS object using AFC ADSI.
//    Any comment beginning "// ..." means Java codes lines have
//        been deleted for simpler logic flow. For complete code,
//        see ADSI samples.
//
// Copyright © 1997 Microsoft Corporation
// All Rights Reserved
//
// ... import statements

class bind
{
    private ADsConn m_conn = null;
    private IADs    m_adsobj = null;
    private IADsOpenDSObject    m_namespace = null;

    String iniFileName = ".\\jadsi.ms";
    // ... Initializations

    public static void main(String args[])
    {
        bind bindObject = null;

        // ... Check the arguments on input
        if (bindObject != null) {
            bindObject.bindToObject();
    }

    }

    public bind()
    { // ... Prompt for ADsPath, UserName, Passwd }

    public bind(String args[])
    { // ... Get ADsPath, UserName, PassWd from the args array }

    public void bindToObject()
    {
    try  {  // Process the .ini file to set up AFC ADSI
        m_conn = new ADsConn(iniFileName);
    }  // ... plus error handling

    try {  // instantiate the LDAP namespace object
        m_namespace = (IADsOpenDSObject)m_conn.getObject("LDAP:");

        // Fetch a DS object using the specified parameters.
        m_adsobj = (IADs)m_namespace.OpenDSObject(ADsPath, UserName,
                    PassWd, 0);
        // ... print out IADs object properties
    } // ... plus error handling

    // ... Exit code
    }
}


Query Examples Using ADO

The following code shows how to connect to a directory service, access an ADSI provider, and query an object.

Dim QuerySTring As String
Dim MyConnection As New Connection
Dim MyRecordset As Recordset
Dim i As Integer
Dim Msg As String
Dim vRecsAffected As Variant
Dim liTemp As ListItem

On Error GoTo errhand

' Ask for and get credentials if necessary.

If (fLogin.mylogin) Then

    FormQuery.MousePointer = vbHourglass
    ' Set the query string
    QuerySTring = sDn + ";" + sQuery + ";" + sReturnAttributes
    ' Add adspath to the return string.
    QuerySTring = QuerySTring + ",adspath"
    ' Add the scope
    If iScope = SCOPEBASE Then
        QuerySTring = QuerySTring + ";Base"
    ElseIf iScope = SCOPE1LVL Then
        QuerySTring = QuerySTring + ";OneLevel"
    ElseIf iScope = SCOPESUBTREE Then
        QuerySTring = QuerySTring + ";SubTree"
    Else
        MsgBox "internal err"
    End If

    ' Set the connection object props
    MyConnection.Provider = "ADsDSOObject"
    MyConnection.Open "ADs Provider", szUserName, szPassword
    Set MyRecordset = MyConnection.Execute(QuerySTring, vRecsAffected, adCmdUnspecified)
    ' Prepare listview
    FormQuery.ListView1.ColumnHeaders.Clear
    FormQuery.ListView1.ListItems.Clear
    ' Put in listview column headers for the number of attributes.

    For i = 0 To MyRecordset.Fields.Count - 1
        FormQuery.ListView1.ColumnHeaders.Add , , CStr(MyRecordset.Fields(i).name), _
         (FormQuery.ListView1.Width / MyRecordset.Fields.Count)
    Next i

    ' Loop through each row returned and display.
    While Not MyRecordset.EOF
        For i = 0 To MyRecordset.Fields.Count - 1
            On Error GoTo NOHANDLE
            DoEvents                'Handle events
            If i = 0 Then
                ' Add main column item to list view.
                If Not IsNull(MyRecordset.Fields(i)) Then sString = CStr(MyRecordset.Fields(i)) Else sString = "Null"
                Set liTemp = FormQuery.ListView1.ListItems.Add(, , sString)
            Else
                If Not (IsEmpty(MyRecordset.Fields(i))) Then
                    If Not (IsNull(MyRecordset.Fields(i))) Then sString = CStr(MyRecordset.Fields(i)) Else sString = "Null"
                    liTemp.SubItems(i) = sString
                End If

                On Error GoTo errhand
            End If
        Next i
        iQueryAdsPathArrayIndex = i
        sString = ""

        MyRecordset.MoveNext
    Wend

    FormQuery.MousePointer = Default
    If FormQuery.ListView1.ListItems.Count = 0 Then MsgBox "No records returned"

End If


COM Vtable Examples

This section contains short code examples in C or C++ for ADSI client applications and one longer example for query.


COM Vtable Examples - Querying a Namespace

See Using IDirectorySearch for Querying.


COM Vtable Examples - Setting Properties

IADsUser  *pUser;
BSTR      bstrDescription;
BSTR      bstrTitle;
VARIANT   vHints;

//
// Bind to the user object.
//
ADsGetObject(TEXT("WinNT://MyDomain/Users/John"),
             IID_IADsUser,
             (void**)&pUser);

//
// Allocate the strings for the changes.
//
bstrDescription = SysAllocString(TEXT("Johnny"));
bstrTitle = SysAllocString(TEXT("General Manager"));
bstrPassword = SysAllocString(TEXT("PW.123"));

//
// Change the Description and title of the user.  The changes
// are made in the local cache only.
//
pUser->put_Description(bstrDescription);
pUser->put_Title(bstrTitle);

//
// Call SetInfo to commit the changes to the network.
//
pUser->SetInfo();

//
// Now change the user's password - this non-property method
// executes immediately.
//
pUser->SetPassword(bstrPassword);
//
// Refresh the ADSI object with a call to GetInfo.
//    No hints are specified.
//
VariantInit(&vHints);
V_VT(&vHints) = VT_EMPTY;
pUser->GetInfo(vHints);
//
// Clean up and reuse the strings.
//
SysFreeString(bstrDescription);
SysFreeString(bstrTitle);
SysFreeString(bstrPassword);
//
// Retrieve values from the object.
//
pUser->get_Description(&bstrDescription);
pUser->get_Location(&bstrTitle);
printf("%s, %s", bstrDescription, bstrTitle);
//
// Release all interfaces.
//
pUser->Release();


COM Vtable Examples - Adding Users to a Group

IADsGroup *pGroup;

//
// Bind to a known group object.
//
ADsGetObject(TEXT("WinNT://MyDomain/Groups/Administrators"),
             IID_IADsGroup,
             (void**)&pGroup);

//
// Add Jane and John to the Administrators group.
//
pGroup->Add(TEXT("WinNT://MyDomain/Users/John"));
pGroup->Add(TEXT("WinNT://MyDomain/Users/Jane"));

//
// Cleanup.
//
pGroup->Release();


COM Vtable Examples - Removing Users from a Group

//
IADsGroup *pGroup;
//
//
// Bind to a known group object.
//
ADsGetObject(TEXT("WinNT://MyDomain/Groups/Administrators"),
             IID_IADsGroup,
             (void**)&pGroup);
//
// Remove Jane and John from the Administrators group.
//
pGroup->Remove(TEXT("WinNT://MyDomain/Users/John"));
pGroup->Remove(TEXT("WinNT://MyDomain/Users/Jane"));
//
// Cleanup.
//
pGroup->Release();


COM Vtable Examples - Enumerating Users in a Group

IADsGroup *pGroup;
IADsMembers *pMembers;
IEnumVARIANT *pEnum;
VARIANT vMember;
HRESULT hr;

//
// Bind to a known group object.
//
ADsOpenObject(L"LDAP://MyMachine/CN=Domain Guests,CN=Users,DC=MyDomain,DC=ArcadiaBay,DC=com", L"Administrator", L"", ADS_SECURE_AUTHENTICATION, IID_IADsGroup, (void**)&pGroup);

//
// Get the Members collection.
//
pGroup->Members(&pMembers);

//
// Get an enumerator.  ADsBuildEnumerator is a helper
// function supplied by ADSI.  See Appendix B for more
// details.
//
ADsBuildEnumerator((IADsContainer *)pMembers, &pEnum);

//
// Enumerate through all the children of the container.
// ADsEnumerateNext is a helper function supplied by ADSI.
// See Appendix B for more details.
//
do {

    hr = ADsEnumerateNext(pEnum, 1, &vMember, NULL);

    if (SUCCEEDED(hr)) {
        printf("%S\n", vMember.bstrVal);
    } // if

} while (SUCCEEDED(hr)); // do

//
// Cleanup.
//
pEnum->Release();
pMembers->Release();
pGroup->Release();


COM Vtable Examples - Creating a User Object

IADsContainer *pContainer;
IADs     *pNewObject;
IADsUser *pUser;
IDispatch *pDisp;

//
// Bind to the known container.
//
ADsGetObject(TEXT("WinNT://Mydomain"),
             IID_IADsContainer,
             (void**)&pContainer);
//
// Create the new wrapper.
//
pContainer->Create(TEXT("user"),
                   TEXT("Jane"),
                   &pDisp);
//
// Get the IADsUser interface from the wrapper.
//
pDisp->QueryInterface(IID_IADsUser, (void**)&pUser);
//
// Write it back to the DS.
//
pUser->SetInfo();
//
// Set Jane's password.
//
pUser ->SetPassword(TEXT("PW.1234"));
//
// Cleanup.
//
pContainer->Release();
pNewObject->Release();
pUser->Release();


COM Vtable Examples - Deleting a User Object

IADsContainer *pContainer;
BSTR     bstrContainerPath;
IADsUser *pUser;
BSTR     bstrUserName;
//
// Bind to the known object.  In this case, the explicit path
// is known, but typically the object will be discovered
// using query or enumeration.
//
ADsGetObject(TEXT("WinNT://MSFT/Users/Jane"),
             IID_IADsUser,
             (void**)&pUser);
//
// Get the container object path.
//
pUser->get_Parent(&bstrContainerPath);
//
// Get the container object.
//
ADsGetObject(bstrContainerPath,
             IID_IADsContainer,
             (void**)&pContainer);
//
// Get the user object's name.  (This step is unnecessary in
// this example since the name is known, but is included to
// indicate that either the name needs to be known, or to have a ptr
// to the object).
//
pUser->get_Name(&bstrUserName);
//
// Release the user object.  Note that this will delete
// the underlying DS object, so do not hang on to the user
// component.
//
pUser->Release();
//
// Delete the user.
//
pContainer->Delete(TEXT("user"),bstrUserName);
//
// Cleanup.
//
SysFreeString(bstrContainerPath);
SysFreeString(bstrUserPath);
pContainer->Release();


Implementing ADSI Providers

Active Directory™ Service Interfaces are COM interfaces that wrap directory service objects to make them accessible to clients of directory services. By supplying an implementation of ADSI, you expand your clientele to the set of ADSI client applications.

As with any COM implementation, you can write an ADSI provider in many languages. The ADSI COM interfaces are defined as dual interfaces that allow both run-time and compile-time name resolution and can be called by Automation-compliant languages like Microsoft® Visual Basic®, Visual Basic® Scripting, and Java, and also the more performance and efficiency-conscious languages such as C and C++. ADSI clients also include web applications using Active Server Pages and administration snap-ins through the Microsoft Management Console.

Because ADSI supplies its own OLE DB provider, implementing the search functionality defined by IDirectorySearch also enables ADSI clients to query your directory service for information of interest.

All directory service objects can be represented through a generic ADSI object supporting IDirectoryObject. ADSI supplies the building blocks necessary to represent the features and services of any directory service.

In addition, the ADSI meta-interfaces represent common objects used by directory administrators. You map the properties of the meta-interfaces to the properties supported by your directory service. ADSI clients programming to the Active Directory Service Interfaces gain access to your directory service as soon as the provider is installed and the system re-started.

If your directory service supports a schema representation, supporting the schema management interfaces makes your namespace directly accessible to directory service browsers. By publishing your features through the schema, clients can query your directory service on-line and take advantage of the services you offer. Because of the on-line schema availability and the COM interface advantage, you can continue to make new functionality available to your client software while supporting down-level versions.


Provider Minimum Requirements

To write an ADSI provider, implement the Active Directory™ Service Interfaces as described in the following sections.

  • Core Implementation briefly lists those software elements that an ADSI provider must support.
  • Optional Implementation briefly lists those software elements that an ADSI provider is strongly encouraged to support.
  • Custom properties are those properties and interfaces exposed by each provider to provide programmability of the native features of the directory service.
  • Dual Interfaces describe the ADSI convention to supply interfaces that are accessible both to Automation and non-Automation compliant controllers.


Core Implementation

An ADSI provider supports the following features.

  • ADsPath strings in both COM and URL format. By definition, an Active Directory object can always be retrieved using an ADsPath. For the ADsPath syntax definition, see ADsPaths. Providers support whatever syntax their directory service requires using the ParseDisplayName implementation described in the next bulleted item.
  • A top-level Namespace object that supports IParseDisplayName::ParseDisplayName and IADsOpenDSObject. This object contains the root nodes for this namespace. Part of the ParseDisplayName implementation must include a parser that checks for syntax errors for its own namespace. This object must also support IADs and IADsOpenDSObject.
  • The IADs interface. This includes a simple property cache implementation through IADs::GetInfo and IADs::SetInfo. Operations that get or set object properties occur in cached mode. Cache values are written to the underlying directory store during SetInfo. An ADSI method, however, is not cached, but executes immediately.
  • The IADsPropertyList , IADsPropertyEntry and IADsPropertyValue interfaces for directly accessing and enumerating the properties in the property cache. For directory services that do not expose a schema, this interface allows you to manipulate the properties of an object.
  • The IDirectoryObject interface for non-Automation clients. This uses on-the-wire protocol for maximum performance and bypasses the property cache.
  • The IADsContainer interface. Every Active Directory object has a parent container that controls its lifetime. Note that on ADs container objects, GetInfo affects only the container properties and not its contents. SetInfo on ADs container objects affects only newly created objects and the container properties, and does not affect objects that already exist within the container.
  • The IDispatch interface. This is the Automation interface for languages like Java and Visual Basic that do not use compile-time binding. Related to this is the type library information that a provider must supply. For more information, see Implementation Issues for ADSI Providers.
  • A schema class container object and the appropriate syntax, property, and schema class objects that support IADsSyntax, IADsProperty, and IADsClass respectively. At a minimum, each root node must contain its own schema class container object.
  • The IADsMembers interface, if any supported attributes are collections of ADSI objects, such as security groups.
  • The IADsCollection interface, if any supported attributes are collections of the same directory element type with Add and Remove capability.
  • The IEnumXXXX enumeration support for all specific enumerator objects.
  • Routines to map syntax and convert native data to the VARIANT data type.
  • Follow ADSI conventions for provider-supplied ADSI objects. Include help files and type libraries documenting the interface properties and methods. See
  • Meta-Schema Interfaces, a minimal implementation required. Because you map your directory service names for the appropriate attributes to the ADSI property names when implementing the meta-interfaces, client applications can display directory service objects in a uniform manner across all directory services.

As with any COM implementation, you must return E_NOTIMPL for unimplemented interfaces and E_ADS_PROPERTY_NOT_SUPPORTED for unimplemented properties. For more information about dual interfaces and how they affect property implementation, see Dual Interfaces.


Optional Implementation

The following features are optional, but highly recommended.

  • The IDirectorySearch interface for non-Automation clients. Because the ADSI OLE DB provider uses IDirectorySearch to present queries and collect results from the underlying directory service, providers that implement this interface gain access to OLE DB style databases for no extra effort.
  • The IADsSecurityDescriptor, IADsAccessControlList, and IADsAccessControlEntry interfaces. Providers for directory services that support ACL-based object security are strongly encouraged to implement this additional functionality.


Custom Properties

Providers can expose additional objects, interfaces, and properties not defined by the ADSI meta-schema.

Further details on implementation guidelines for providers can be found throughout ADSI Reference under Notes to Implementers. Examples of most of the items listed above can be found in ADSI Example Provider Component.


Dual Interfaces

Use COM interfaces to access the properties and methods on any provider ADSI objects. A read-only property maps to an interface entry of the form get_<PropertyName>. A read/write property maps to two interface entries of the form get_<PropertyName> and put_<PropertyName>.

All methods on a COM interface must:

  • Return an HRESULT value to indicate success or failure. The HRESULT type is discussed in the COM specification.
  • Return E_NOTIMPL for interfaces that have not been implemented.
  • Return E_ADS_PROPERTY_NOT_SUPPORTED for interface properties that are not supported.

To retain compatibility with Automation controllers, all parameters and return types should be within the subset defined by the Automation VARIANT data type. For more information, see VARIANT and VARIANTARGS in the Microsoft Platform SDK.

A provider Active Directory object can expose interfaces that use data types other than those in the VARIANT subset. However, Automation controllers such as Visual Basic and Java will not be able to call those interfaces. Since all ADSI provider interfaces are derived from IDispatch, they can be used as IDispatch interface pointers.


Provider Overview

Besides the Provider Minimum Requirements, other information you may find useful as a provider writer includes:

  • ADSI Component Interaction describes the context in which the provider component runs.
  • Schema Interfaces discuss the schema management interfaces from the provider's point of view.
  • Enumerating Container Objects describes the requirements to support enumerations.
  • Provider Registry Information lists the registry entries an ADSI provider uses.
  • Support for Queries
  • Implementation Issues for ADSI Providers includes notes to providers about various software issues.


ADSI Component Interaction

The Active Directory router component populates an ADSI provider table from the installed ADSI providers listed in the registry when it receives its first request from the client application. Registry information can be found in Installing the Example Provider Component.

Operations that make a request from a directory for a pointer to an interface on an Active Directory object come through a function (GetObject in Visual Basic or Java or ADsOpenObject or ADsGetObject in C or C++), or an interface method (IADsContainer::GetObject). In the following figure, the ADSI client application passes such a bind request to the ADSI router component (1). The router component identifies the ProgID for the provider from the first part of the ADsPath and uses CLSIDFromProgID to find the matching CLSID in the registry(2) and loads the proper provider component (3).

ADSI Component Interactions in the Example Provider.

In the preceding figure, the provider component creates an Active Directory object representing the named directory element. The ADSI support component does a QueryInterface on the requested interface identifier. Once a pointer to that interface is retrieved (4), as with all COM client/server implementations, it is then passed back to the client (5), and from then on the client application works directly with the provider component (6).


Schema Interfaces

The schema interface IADsClass is available to represent a directory service class. It is used to represent a provider's directory service class. The IADsProperty interface represents a single or multi-valued property from the directory service. The IADsSyntax interface represents a single VARIANT type.

Providers supplying their own ADSI implementations supply schema management objects supporting these interfaces that reflect their underlying namespace.

Interfaces defined by ADSI can support specific properties and syntaxes, such as IADsUser and IADsOU. For the complete list, see ADSI Interfaces. Providers can choose to extend an ADSI definition by using the methods that create and access properties IADs::Get, IADs::GetEx, IADs::Put, and IADs::PutEx. Providers can also choose to support additional properties through additional interfaces.

The schema container is used to attach a set of schema definitions to part of a provider's namespace tree. Typically, each instance of a namespace has its own schema. For example, in the following figure, the ADSI example provider defines a schema container in each of the root nodes "Seattle" and "Toronto".

ADSI Schema Objects in the Example Provider

An ADSI provider component with a complex namespace might allow multiple schemas to exist in a namespace instance, each at a different part of the tree. An object's IADs::Schema property, however, always names its own the schema definition.


Schema Extensions

Because of the architecture of the ADSI schema, new schema classes can be added to the schema class container and new properties to an existing schema class object at run time. The latter ability requires no new code, and for namespaces that allow extensible directory services, this is an important feature. The provider component must allow for this extensibility and know where to access and store the class instance and the values of its properties. In a typical extensible directory service, this information is stored in the directory service database in the same manner as any other schema class and property definitions.

Note In COM interface terminology, only properties can be added to an existing schema class, not methods. Adding a new method would require adding a new implementation of that method and thus require additional code.

For an example of a specific provider schema, see Schema Management.


Enumerating Container Objects

By convention, all items in an enumeration in ADSI must be of the same Automation data type. For example, an enumeration should not return some items as VARIANTs of type VT_I4 and others as VARIANTs of type VT_BSTR.

To enumerate a list of items that an object maintains, a client requests the creation of an enumeration object for the specific type of information being listed. In ADSI, the client may list the objects in namespace objects, generic container objects, collection objects, member objects, or schema objects. ADSI provides a filter that can be set and modified to limit the matches in any enumeration though IADsContainer::Filter. Examples of implementations of enumerator objects can be found in the example provider component code for the following ADs container objects.

Object type | code
Generic Container | cenumobj.cpp
Namespace Container | cenumns.cpp
Schema Container | cenumsch.cpp

For client-side information, see About Collections and Groups .


Provider Registry Information

Under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ADs\Providers subkey, an ADSI provider must have the following subkeys and values:

ProgIDNamespace = Friendly Name

Under the HKEY_CLASSES_ROOT\CLSID subkey, provider namespace objects must have the following subkeys and values:

ProgIDNamespaceCLSID = FriendlyDisplayName

ProgIDNamespaceCLSID\InProcServer32 = ProviderDLLFilename

ProgIDNamespaceCLSID\InProcServer32\ThreadingModel=Both

ProgIDNamespaceCLSID\ProgID= ProviderNamespaceObjectName

ProgIDNamespaceCLSID\TypeLib=ProviderTypeLibCLSID

ProgIDNamespaceCLSID\Version = ProviderVersionNumber

Under the HKEY_CLASSES_ROOT\CLSID subkey, provider top-level objects must have the following subkeys and values:

ProgIDCLSID = FriendlyDisplayName

ProgIDCLSID\InProcServer32 = ProviderDLLFilename

ProgIDCLSID\InProcServer32\ThreadingModel=Both

ProgIDCLSID\ProgID= ProviderProgIDObjectName

ProgIDCLSID\TypeLib=ProviderTypeLibCLSID

ProgIDCLSID\Version = ProviderVersionNumber

After installing ADSI SDK, the registry contains the following values under HKEY_LOCAL_MACHINE:

    SOFTWARE
        Microsoft
            ADs
                Providers
                    LDAP      <No Name>:REG_SZ:LDAPNamespace
                    NDS       <No Name>:REG_SZ:NDSNamespace
                    NWCOMPAT  <No Name>:REG_SZ:NWCOMPATNamespace
                    WinNT     <No Name>:REG_SZ:WinNTNamespace

For a specific example, see Installing the Example Provider Component.


Support for Queries

By implementing the IDirectorySearch interface, ADSI providers gain access to a subset of OLE DB read-only functionality with no extra effort.

The ADSI implementation of the methods of IDirectorySearch itself uses the OLE DB interfaces described in ADSI Version 1.0 included the following list of COM objects:

  • Data Source Object (the directory service), supporting IDBInitialize, IDBCreateSession, IDBProperties, and IPersist.
  • DBSessions Object, supporting IOpenRowSet, ISessionProperties, and ICreateCommand.
  • Command Object, supporting ICommand, IAccessor, IColumnsInfo, ICommandProperties, ICommandText, and IConvertType.
  • Rowset Object, supporting IAccessor, IColumnsInfo, IConvertType, IRowset, and IRowsetInfo.

For more information about OLE DB, see OLE DB Programmer's Reference in the Microsoft Platform SDK.


Implementation Issues for ADSI Providers

In addition to issues listed here, details for implementations can be found in the ADSI Reference under Notes to Implementers and When to Implement.

The recommended way to implement the ADSI interfaces is to first implement the pure COM interface IDirectoryObject. By providing this "minimal-overhead" layer, you supply client applications with control most needed for performance reasons and supply your own implementation with the most flexibility.

Next implement the fundamental ADSI interfaces, IADs, IADsContainer, IADsCollection, and the IADsPropertyValue, IADsPropertyEntry, IADsPropertyList property cache interfaces. IADsGroup and IADsMembers are also interfaces in frequent demand by system administration software.

Implement the schema management interfaces if your directory service has an underlying schema: IADsClass, IADsProperty, IADsSyntax. If there is no underlying schema, you can use these interfaces to abstract the classes and properties used by the directory service as if there were. Schemas are a great means of publishing the features of your directory service to ADSI clients.

Next implement the meta-interfaces so that your directory service features are accessible to ADSI clients that use a single user interface.

Collections

ADSI provider components can follow one of three models for caching collections during enumeration. The choice of a caching model determines the behavior of ADSI when an object in a collection is deleted from the underlying directory service "outside" of ADSI.

The caching models are:

  1. Collections are cached in advance. The collection of object instances is retrieved from the underlying directory service in its entirety when IADsCollection::get__NewEnum is called to create a new enumerator object. If the source object for an Active Directory object instance in the retrieved collection is deleted from the underlying directory service the client does not become aware of the deletion until a IADs::GetInfo or IADs::SetInfo attempts to access the collection.
  2. Collections are incrementally cached. The collection is retrieved from the underlying directory service one object at a time when IEnumVARIANT::Next is called. IEnumVARIANT::Reset will return to the beginning of the collection in the cache and IEnumVARIANT::Next will return cached objects until the end of the cache is reached, at which point new objects will be added from the underlying store. Once an Active Directory object instance is in the cache the client will not become aware of its deletion from the underlying directory service until an IADs::GetInfo or IADs::SetInfo attempts to access the object.
  3. Collections are not cached. The collection is retrieved from the underlying directory service one object at a time when IEnumVARIANT::Next is called. IEnumVARIANT::Reset will return to the beginning of the collection in the underlying store. IEnumVARIANT::Next and IEnumVARIANT::Reset operations cannot retrieve deleted objects, since the objects are fetched on demand from the underlying directory service. Only the "current" object is cached; if the current object is deleted, the client will not become aware of its deletion from the underlying directory service until a IADs::GetInfo or IADs::SetInfo attempts to access the object.

Regardless of the caching model, it is important to note that ADSI enumeration returns Active Directory service interfaces to the caller. To avoid the overhead of obtaining a new interface pointer, ADSI applications should cache the returned interface pointers for object(s) they intend to manipulate. For example, a Visual Basic program that enumerates a container and populates a listbox with names can cache the interface pointers associated with the names for later use. This approach will provide greater performance than populating the listbox during enumeration and obtaining a new interface pointer when the user makes a selection.

About Dispatch IDs

IDispatch is an Automation interface defined by COM for controllers that do not use COM interfaces directly. Accessing an object through IDispatch is called name-bound or late-bound access, since it occurs at run time ("late") and uses string names of properties and methods to resolve references ("name"). At run time, clients pass the string name of the property or method they wish to call into the IDispatch::GetIDsOfNames() method. If the property or method exists on the object, the dispatch identifier (dispID) of the corresponding function is retrieved. The dispID is then used to execute the function through IDispatch::Invoke(). Using IDispatch, properties and methods on the interfaces exposed by a single object appear as a flat list. Because name-bound access requires two function calls, it is less efficient than using a COM interface directly. Clients are encouraged to use the ADSI COM interfaces on the objects when performance is a consideration. Advanced Automation controllers such as Visual Basic 4.0 and Java can call other COM interfaces as well as IDispatch, if the interfaces comply with the Automation constraints for data types and parameter passing.

ADSI providers generate dispIDs dynamically for each Active Directory object. The dispIDs retrieved through IDispatch::GetIDsOfNames for a given object are the generated values, but not the values that are in the IDL for the object. IDispatch users must call GetIDsOfNames to obtain valid dispIDs at run time.

Type Information and Type Libraries

The ADSI SDK supplies a type library, ActiveDs.tlb, that documents all the standard interfaces supported by ADSI. A provider must supply a similar type library for all interfaces found in ActiveDs.TLB, plus any additional type information for the interfaces that are implemented within the provider component. Additional information about type libraries can be found in Generating a Type Library With MIDL under Tools Guide in the Microsoft Platform SDK.

An example of IDL code:

[ object, uuid(IID_IADsXYZ), oleautomation, dual ]
interface IADsXYZ: IDispatch
{
// Read-only properties.
[propget]
HRESULT AReadOnlyProp ([out, retval]BSTR *pbstrAReadOnlyProp);

// Read/write properties.
[propget]
HRESULT AReadWriteProp ([out, retval]long *plAReadWriteProp);
[propput]
HRESULT AReadWriteProp ([in]long lAReadWriteProp);

// Methods.
HRESULT AMethod ([in]DATE dateInParameter,
[out, retval]BSTR *pbstrReturnValue);
};

Thread Safety

Windows NT/Windows 2000 and Windows 95 support multiple threads in a single process. ADSI providers may therefore be used in multithreaded applications and must allow for this.

The Component Object Model describes the following three different threading models. COM programs indicate which model is in use when initializing the COM library using the CoInitialize and CoInitializeEx functions.

  • Single threading. The single threaded model assumes a single thread of execution in a process, further assuming that COM data structures in a process need no access serialization.
  • Apartment threading. A COM object is associated with the thread that created it. Calls to an object on another thread must be executed by the thread that created that object. To accomplish this, the source thread invokes a client proxy which marshals the method call and delivers it to a server stub function in the destination thread through the Win32 message queue associated with the destination thread.
  • Free threading. COM objects are assumed to be thread safe. Multiple threads are allowed access to any object in the process with no serialization imposed.

ADSI does not assume any particular threading model. Writers of provider components should assume the free threading model and guarantee the consistency of their internal data structures by protecting them from thread-unsafe (that is, uncoordinated) updates through the use of synchronization objects such as critical sections or semaphores.

Object Locking

ADSI does not impose or define an object-locking scheme. Providers for namespaces that support access serialization using locking can expose the underlying locking scheme through provider-specific extensions to ADSI.

Property Names Within a Schema

ADSI represents properties as property objects within the ADSI schema container. This requires that property names be unique within each schema container. It is up to the provider to ensure there are no name collisions.

Primary Interface

When a provider does not know what interface should be returned as the primary interface, IID_IADs should be returned. This provides name-bound access to all properties of an object through IDispatch and the IADs::Get, IADs::GetEx, IADs::Put, and IADs::PutEx methods.


ADSI Example Provider Component

Active Directory™ Service Interfaces provider writers will find this section of particular interest, because it describes the ADSI example provider component in depth. ADSI provider writers can install the example provider and use the code framework as a basis to create their own implementation. The following topics are discussed:

Installing the Example Provider Component describes how to install the ADSI sample provider and its mock directory. This section includes a list of the registry settings.

Directory Definition describes the directory entries defined by the example provider component.

Schema Management describes how each element in the example provider component directory service can be represented by a specific type of Active Directory object.

Binding to an Active Directory Object describes how, given an ADsPath string, the example provider component identifies that element, creates the appropriate type of Active Directory object for it, and retrieves the requested type of interface pointer on that object to pass back to the ADs client software.

Enumerator Objects lists the specific types of enumerations supplied by the example provider component and in which source code the implementations can be found.

Code Overview provides a task-level description of each part of the example provider component.

Code Details lists the software modules and their contents.


Installing the Example Provider Component

After installing the ADSI SDK, from the installation directory, change directories to the samples\activeds\sampprov\setup subdirectory. Run install.bat as described in the README.TXT instructions.

For registry settings, under the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ADs\Providers subkey, the ADSI example provider component has the following subkeys and values:

ProgIDNamespace = SampleNamespace

Under the HKEY_CLASSES_ROOT\CLSID subkey, provider namespace objects must have the following subkeys and values:

SampleNamespaceCLSID= Sample Namespace Object
SampleNamespaceCLSID\InProcServer32 = adssmp.dll
SampleNamespaceCLSID\InProcServer32\ThreadingModel=Both
SampleNamespaceCLSID\ProgID= Sample
SampleNamespaceCLSID\TypeLib=ProviderTypeLibCLSID
SampleNamespaceCLSID\Version = 0.0

Under the HKEY_CLASSES_ROOT\CLSID subkey, provider top-level objects must have the following subkeys and values:

SampleCLSID = Sample Provider Object
SampleCLSID\InProcServer32 = adssmp.dll
SampleCLSID\InProcServer32\ThreadingModel=Both
SampleCLSID\ProgID= Sample
SampleCLSID\TypeLib=ProviderTypeLibCLSID
SampleCLSID\Version = 0.0

For the specific example of the example provider component dll, the following registry entries are needed:

HKEY_LOCAL_MACHINE
    SOFTWARE
        +CLSID
        Sample
            Clsid          <No Name>:REG_SZ{323991f0-…}
        SampleNAMESPACE
            Clsid          <No Name>:REG_SZ{51d11c90--…}

Other registry entries for the example provider component exist because the example provider component happened to implement its directory hierarchy in the registry. This in no way implies other providers would want to or should do the same.


Directory Definition

The example provider component uses a relatively simple directory design to clarify the relationship between components and to show the minimum requirements necessary to be an ADSI provider.

The "directory" for the example provider component consists of two root nodes: "Seattle" and "Toronto". Seattle contains two more sublevels, "Bellevue" and "Redmond". Each of these entries contains several user accounts. The "Toronto" entry has no further sublevels, but directly contains several user accounts. The following figure shows these two root nodes connected to a network.

In hierarchical terms, the Namespace node contains "Seattle" and "Toronto". "Seattle" contains "Bellevue" and "Redmond". "Bellevue" and "Redmond" each contain a set of user accounts. "Toronto" directly contains the user accounts with no intermediate organizational nodes.

The example provider component represents this structure with only two Active Directory object types: a container object and a leaf object. "Seattle", "Toronto", "Bellevue", and "Redmond" are container objects and each user account is a leaf object.

The example provider component creates a schema class called "Organizational Unit" for a container object type and a schema class called "User" for a user account.

The properties for each schema class, their methods, and the rules that govern the containment relationships for these objects are all defined in Schema Management.


Schema Management

The ADSI example provider component defines the schema classes "Organizational Unit" and "User" and manages these schema classes in the following way.

The "Organizational Unit" schema class is represented by an ADs container object and can contain other "Organizational Units" and "Users". The container object automatically supports the interfaces IUnknown, IDispatch, IADs, and IADsContainer. The "User" schema class is represented by a generic Active Directory object and does not contain any other types of objects. In the example provider component, the User object is implemented as a generic Active Directory object that supports the interfaces IUnknown, IDispatch, and IADs . Note that the User object in this case does not support the IADsUser interface.

The example provider component must also implement the ADs namespace object, which supports the interfaces IUnknown, IADs, IADsContainer, IADsOpenDSObject, and IDispatch.

The details of the schema representing the two example provider component schema classes are shown in the following figure.

ADSI Example Provider Component Schema

Note that in the preceding figure the "Organizational Unit" schema class defines three properties: "Description," "Headcounts," and "ID." The "User" schema class defines four properties: "ID," "Name," "PhoneNumber," and "Title." The "ID" property is shared by both schema classes. Each property is defined by either the "String" or the "Integer" syntax object.

Note Syntax objects are not present in the first release of the example provider component. However, in most Microsoft® ADSI schema implementations, the syntax objects are included in the schema container object, just as the schema class and property objects are. For this reason, they are shown here.

This provider component makes the schema information accessible to the ADSI client application in the form of ADs class objects, ADs property objects, and ADs syntax objects, all within a schema class container object, so that schema information can be retrieved at run time.

The ADsPaths for the schema class container objects defined for the example provider component are "Sample://Seattle/schema" and "Sample://Toronto/schema.". In this example, the contents of the schemas are identical.


Binding to an Active Directory Object

The most common way to bind to an Active Directory object is to use the GetObject function between an ADSI client and an ADSI provider. This is also the easiest way to show how the provider component receives and services requests. Both the ADSI API function ADsGetObject or the Visual Basic® function GetObject follow the same steps for binding.

For this example, assume the ADSI client is an ADSI viewer application that has received the ADsPath "Sample://Seattle/Redmond/AnneA" from its user interface (1). The following figure details the sequence of events by numbering the flow arrows.

ADSI Components: Detailed View

In the preceding figure, the client initiates the request for an interface pointer on the Active Directory object represented by the ADsPath "Sample://Seattle/Redmond/AnneA" from ADSI services (2). During initialization, the services software populated a table of installed provider programmatic identifiers (ProgIDs) from the registry ("LDAP:","Sample:", "WinNT:","NDS:", and so on) and paired them with the matching CLSIDs which identify the appropriate software module.

The ADSI server makes sure the ProgID, in this case "Sample:", exists in the ADsPath. It then creates a bind context to optimize further references to this object, and calls the standard COM function MkParseDisplayName to create a COM moniker that can be used to find and bind to the object representing user "AnneA".

(In the following discussion, the file names of the example provider component source code are included in parentheses where appropriate.)

As in other COM server implementations, MkParseDisplayName examines the ProgID, and looks up the proper CLSID in the registry (3) to find the corresponding provider-supplied class factory code (cprovcf.cpp) in the appropriate provider implementation (4). This code creates an initial object that implements the IParseDisplayName::ParseDisplayName method (cprov.cpp). The provider's implementation of ParseDisplayName resolves the path name in the provider's own namespace. This eventually calls ADsObject and invokes the parser supplied with the provider component (parse.cpp) to check that the ADsPath is syntactically correct for this namespace.

GetObject then determines if the object being requested is a top-level object, a schema object, or some other type of object. If either of the first two, that type of schema class object is created and the appropriate interface pointer retrieved. Otherwise the Sample directory path is created from the ADsPath (for example, "\Seattle\Redmond\AnneA", but in a different directory service it might have had to be "\OU=Seattle\OU=Redmond\CN=AnneA") and control passes to SampleDSOpenObject which opens the object in the example data storage and also retrieves its object type (in this case, "User") (5).

With the information gathered so far, a new object is created (6) to represent the item described by the ADsPath, and a pointer to the IUnknown interface on that object is retrieved. In this case, a generic Active Directory object is created that supports the IUnknown and IADs methods (cgenobj.cpp, core.cpp). The CSampleDSGenObject::AllocateGenObject routine reads the type library information to create the proper dispatch entries for these new objects in order to support IDispatch.

Wrapping this interface pointer into a moniker completes the ResolvePathName (cprov.cpp) function and parsing the display name is done.

All the COM objects created during this process are cached for performance reasons and managed through the binding context. This improves performance for other operations on the same object that immediately follows the moniker binding.

This well-formed Active Directory object is now queried for the interface identifier requested for the initial ADsGetObject call and a pointer to that interface is retrieved (7) and passed back through the ADSI server to the client (8). From then on, the client works directly with the provider component through the interface methods until the initial request is satisfied (9).

Further requests for information about this object from the client usually take the form of requests for property gets and puts, all of which are optimized through the provider's implementation of a property cache (cprops.cpp). Intelligently packing and unpacking data (often including copying and freeing structures and strings) between the native data types on the example provider component's operating system and the Automation VARIANT type supported by ADSI takes place before the properties are loaded into the cache (smpoper.cpp).

The example provider component is designed so that the actual calls to the operating system are logically isolated from the rest of the provider component, creating software that's easily portable to more than one operating system (RegDSAPI.cpp).


Enumerator Objects

The example provider component code supplies the implementation of creating enumerator objects for the ADs namespace object (cenumns.cpp), generic ADSI container objects (cenumobj.cpp), ADSI schema container objects (cenumsch.cpp).


Code Overview

The following figure is a conceptual representation of the blocks of code necessary to implement the ADSI example provider component. Each section is described in the following figure. Knowledgeable COM programmers may find this is all they need for an overview of the example provider component. For more details, each code file is detailed in Code Details.

ADSI Example Provider Component Source Code Description

  1. Loading the DLL (libmain.cpp, guid.cpp). The entry point for the DLL. Class factory static object definitions for the two provider objects: Guid.cpp contains the CLSID definitions for the implementations of the various Example provider component objects.
  2. Provider object class factory and creation code (cprovcf.cpp, cprov.cpp, stdfact.cpp). The provider object is the object that supports IParseDisplayName during the moniker binding operations as discussed in Finding and Binding in the Example Provider Component.
  3. Binding to an object (getobj.cpp). This code calls the parser to check that the given ADsPath is syntactically correct, and then performs any necessary mapping from the ADsPath to the native directory service path for the item being created as an Active Directory object. It looks up the schema definition for this type of object and fills in the mandatory properties. After creating the Active Directory object, an interface pointer to IUnknown is retrieved for the caller.
  4. Parser for the provider's namespace (parse.cpp). This is the code invoked by the step above. The parser simply checks that the ADsPath string passed in is syntactically correct for its own namespace.
  5. Class factory, creation, and enumeration for the namespace object (cnamcf.cpp, cnamesp.cpp, cenumns.cpp). The namespace object is a container object that can be enumerated to find all the root node objects for this namespace.
  6. Class factory and creation code for a generic Active Directory object, and class factory, creation and enumeration code for a generic ADs container object (cgenobj.cpp, cenumobj.cpp, common.cpp, core.cpp). This code is executed whenever an Active Directory object is created.
  7. Filtering and enumerating VARIANTs (cenumvar.cpp, object.cpp). When a collection of VARIANT elements of a single type are managed in within ADSI, this code is used.
  8. Globals (globals.cpp). Namespace keywords, syntax mapping structures from native data formats to the ADs Automation VARIANT type are all defined here.
  9. Marshaling/unmarshaling data (pack.cpp, property.cpp, smpoper.cpp). Conversion from native data formats to the supported set of Automation VARIANT types occurs when properties of an object are loaded into the property cache. Other special handling for data must be performed when structures with pointers are copied, deleted, or moved in memory.
  10. Property cache (cprops.cpp). Caching properties is a feature of the ADSI environment. The IADs::GetInfo, IADs::GetInfoEx, and IADs::SetInfo methods act on the property cache.
  11. Memory management (memory.cpp). Using one set of memory functions to allocate and free memory allows the example provider component to track memory use and ensure there are no memory leaks.
  12. Schema objects and management (cschobj.cpp, cprpobj.cpp, cclsobj.cpp, cenumsch.cpp). This includes routines to create, manage, and enumerate the schema objects. This includes schema class objects, property objects, and syntax objects, in addition to being able to enumerate the schema class container object.
  13. Operating-system specific calls (RegDSAPI.cpp). This includes all calls that reference the native operating system. Among other functions, they include functions opening, closing, reading, and modifying objects as well as those accessing the schema and property data. The example provider component happened to simulate a directory hierarchy by using the registry. Only function names should be of much interest to a provider writer.
  14. IDispatch implementation (cdispmgr.cpp). This code accesses the type library information to allow interface methods to be invoked in an Automation-compatible way.


Code Details

This section lists the source code for the ADSI example provider component implementation. All source code references in this document are subject to change and are available in the example code directory included in the ADSI SDK.

Note The IADs methods GetEx and PutEx are not implemented in the ADSI example provider component. This means that the code that implements Active Directory objects that inherit from IADs do not have GetEx and PutEx methods. This includes the schema class object that supports IADsClass, the property object that supports IADsProperty, the generic Active Directory object that supports IADs, and any container object that supports IADsContainer. In addition, syntax objects are not present in the example provider component. However, the ADSI architecture requires the syntax objects to be included in the schema container object, just as the schema class and property objects are.

The following source code files are included in the provider example directory in the Active Directory Service Interfaces SDK.

cclsobj.cpp | Schema class object routines.
cdispmgr.cpp | Dispatch Manager implementation.
cenumns.cpp | Namespace enumeration routines.
cenumsch.cpp | Schema enumeration routines.
cenumobj.cpp | Generic object enumeration routines.
cenumvar.cpp | Base implementation for xxxEnumVariant derived classes.
cgenobj.cpp | Generic object routines.
cnamcf.cpp | Namespace class factory routines.
cnamesp.cpp | Namespace object routines.
common.cpp | Code common to all provider objects.
core.cpp | Implementations for 'core' properties shared by all Active Directory objects.
cprops.cpp | Property cache functionality.
cprov.cpp | Top-level provider object routines.
cprovcf.cpp | Top-level provider object class factory routines.
cprpobj.cpp | Property object routines.
cschobj.cpp | Schema object routines.
getobj.cpp | GetObject functionality.
globals.cpp | ADSI example provider component globals.
guid.cpp | Example provider component CLSIDs and LIBIDs.
libmain.cpp | Libmain for adssmp.dll.
memory.cpp | Example provider component memory management routines.
pack.cpp | Example provider component pack/unpack data in VARIANTs.
parse.cpp | Path name parsing for example provider component namespace.
property.cpp | Get and Put properties by name.
object.cpp | Example provider component object type list code for filtering.
regdsapi.cpp | Example provider component registry directory service APIs.
smpoper.cpp | Data conversion routines.
stdfact.cpp | Standard IClassFactory implementation.
sampleds.reg | Example directory data store registry information. See Installing the Example Provider Component . .


CCLSOBJ.CPP

In the example provider component, schema class object routines are in cclsobj.cpp.

CSampleDSClass::CSampleDSClass
Standard constructor.
CSampleDSClass::~CSampleDSClass
Standard destructor.
CSampleDSClass::CreateClass
Create an ADs schema class object, looking up the attribute definitions by calling SampleDSGetClassDefinition.
CSampleDSClass::CreateClass
Given the attribute definitions, create a schema class object, setting any known attributes, such as the ones listed in IADsClass::MandatoryAttributes.
CSampleDSClass::AllocateClassObject
Create a schema class object and load its type information.
CSampleDSClass::QueryInterface
Return the requested interface pointer if available.
CSampleDSClass:: standard IADs methods.
CSampleDSClass:: standard IADsClass methods.
CreatePropertyList
Create the appropriate list of properties associated with this schema class by calling CreatePropertyEntry.
CreatePropertyEntry
Create one property object in this schema class.
FreePropertyEntry
Free the entry made in CreatePropertyEntry.
MakeVariantFromPropList
Make an array of VARIANTS from the property list created by CreatePropertyList. Can be used in the implementation of IADsClass::MandatoryProperties and elsewhere.


CDISPMGR.CPP

In the example provider component, an example showing the dispatch manager implementation of the IDispatch interface is in cdispmgr.cpp. See IDispatch in the Microsoft Platform SDK.


CENUMNS.CPP

In the example provider component, the enumeration of a namespace object uses the routines from cenumns.cpp.

CSampleDSNamespaceEnum::Create
Create an object to allow enumeration of an ADS namespace object.
CSampleDSNamespaceEnum::CSampleDSNamespaceEnum
Standard constructor.
CSampleDSNamespaceEnum::~CSampleDSNamespaceEnum
Standard destructor.
CSampleDSNamespaceEnum::Next
Retrieve the specified number of elements from the namespace object indicated.
CSampleDSNamespaceEnum::EnumObjects
Manage retrieving the interface pointers to the objects.
CSampleDSNamespaceEnum::FetchObjects
Fetch the set of IDispatch pointers.
CSampleDSNamespaceEnum::FetchNextObject
Fetch the next object. If found, create a generic Active Directory object and retrieve its IDispatch pointer.


CENUMSCH.CPP

In the example provider component, the enumeration of a schema object uses the routines from cenumsch.cpp.

CSampleDSSchemaEnum::Create
Create an object to allow enumeration of an ADS schema class object.
CSampleDSSchemaEnum::CSampleDSSchemaEnum
Standard constructor.
CSampleDSSchemaEnum::~CSampleDSSchemaEnum
Standard destructor.
CSampleDSSchemaEnum::Next
Retrieve the specified number of elements from the schema object indicated.
CSampleDSSchemaEnum::EnumObjects
Manage retrieving the interfaces pointers to the objects of the object type indicated.
CSampleDSSchemaEnum::EnumObjects
Manage retrieving the interface pointers to the objects of the default object type.
CSampleDSSchemaEnum::EnumClasses
Manage retrieving the interface pointers to only the schema class objects that are contained in this object.
CSampleDSSchemaEnum::GetClassObject
Retrieve the next schema class definition; if found, create a schema class object, and return the interface pointer to it.
CSampleDSSchemaEnum::EnumProperties
Manage retrieving the interface pointers to the property objects only that are contained in this object.
CSampleDSSchemaEnum::GetPropertyObject
Retrieve the next property definition; if found, create a schema class object, and return the interface pointer to it.
CSampleDSSchemaEnum::EnumSyntaxes
Manage retrieving the interface pointers to the syntax objects only that are contained in this object.
CSampleDSSchemaEnum::GetSyntaxObject
Retrieve the next syntax definition; if found, create a schema class object, and return the interface pointer to it.


CENUMOBJ.CPP

In the example provider component, the enumeration of a container object uses the routines from cenumobj.cpp.

CSampleDSGenObjectEnum::Create
Create an object to allow enumeration of generic Active Directory objects.
CSampleDSGenObjectEnum::CSampleDSGenObjectEnum
Initialization.
CSampleDSGenObjectEnum::EnumGenericObjects
Manage retrieving the objects.
CSampleDSGenObjectEnum::FetchObjects
Fetch the set of IDispatch pointers that match the filter.
CSampleDSGenObjectEnum::FetchNextObject
Retrieve an object and match against the filter. If it matches, wrap in generic object and return IDispatch pointer.
CSampleDSGenObjectEnum::EnumGenericObjects
Manage retrieving the objects.
CSampleDSGenObjectEnum::Next
Retrieve the specified number of elements from the enumeration object indicated.
CSampleDSGenObjectEnum::IsValidDSFilter
See if object class matches one in the filter list.
CSampleDSGenObjectEnum::BuildDSFilterArray
Manage the filter array.
CSampleDSGenObjectEnum::CreateAndAppendFilterEntry
Add a new object class to the filter and make the filter contiguous.
FreeFilterList
Free the filter.


CENUMVAR.CPP

In the example provider component, the base implementation for xxxEnumVariant derived classes can be found in cenumvar.cpp.

CSampleDSEnumVariant::CSampleDSEnumVariant
Standard constructor.
CSampleDSEnumVariant::~CSampleDSEnumVariant
Standard destructor.
CSampleDSEnumVariant::QueryInterface
Standard IUnknown::QueryInterface implementation.
CSampleDSEnumVariant::AddRef
Standard IUnknown::AddRef implementation.
CSampleDSEnumVariant::Release
Standard IUnknown::Release implementation.
CSampleDSEnumVariant::Skip
Standard IEnumXXXX::Skip implementation.
CSampleDSEnumVariant::Reset
Standard IEnumXXXX::Reset implementation.
CSampleDSEnumVariant::Clone
Standard IEnumXXXX::Clone implementation.


CGENOBJ.CPP

In the example provider component, generic Active Directory object routines are in cgenobj.cpp.

CSampleDSGenObject::CSampleDSGenObject
Standard constructor.
CSampleDSGenObject::~CSampleDSGenObject
Standard destructor.
CSampleDSGenObject::CreateGenericObject
Create an ADS generic object, initializing it as appropriate.
CSampleDSGenObject::SampleDSCreateObject
Create this object in its parent container.
CSampleDSGenObject::SampleDSSetObject
Save the properties of this object (flush the cache).
CSampleDSGenObject::AllocateGenObject
Create a generic object and load its type information.
CSampleDSGenObject::QueryInterface
Return the requested interface pointer if available.
CSampleDSGenObject:: Standard IADs methods, including implementations for:
Get (including mapping from native data type to VARIANT type)

Put (including mapping from VARIANT type to native data type)

GetInfo (refreshing the property cache)

SetInfo (saving the property cache)

CSampleDSGenObject:: Standard IADsContainer methods, including implementations for:
GetObject

get__NewEnum

get_Filter

Create

Delete

ConvertSafeArrayToVariantArray
Utility routine.


CORE.CPP

In the example provider component, the code for initializing a generic object's core properties is in core.cpp.

CCoreADsObject::InitializeCoreObject
Initialize the basic IADs properties for this object, a convenience for object creation.
CCoreADsObject::CCoreADsObject
Standard creator.
CCoreADsObject::~CCoreADsObject
Standard destructor.


CNAMCF.CPP

In the example provider component, an example of the ADs namespace object class factory code is in cnamcf.cpp.

CSampleDSNamespaceCF::CreateInstance
Creates an instance of the class factory for the ADs namespace object.


CNAMESP.CPP

In the example provider component, code managing. the lifetime of the example provider component namespace object is in cnamesp.cpp.

CSampleDSNamespace::CSampleDSNamespace
Standard constructor.
CSampleDSNamespace::~CSampleDSNamespace
Standard destructor.
CSampleDSNamespace:: Standard IADs methods.
CSampleDSNamespace:: Standard IADsContainer methods.
CSampleDSNamespace::AllocateNamespaceObject
Create a namespace object and load its type information.
CSampleDSNamespace::CreateNamespace
Allocate, initialize, and validate a namespace object.
CSampleDSNamespace::QueryInterface
Check for the given IID on this object.


COMMON.CPP

In the example provider component, an example of code common to all generic objects is found in common.cpp. Objects inherit these methods and properties through C++ inheritance rules. These routines include building ADsPath strings, type information routines, and other house-keeping routines.


CPRPOBJ.CPP

In the example provider component, property object routines are in cprpobj.cpp.

CSampleDSProperty::CSampleDSProperty
Standard constructor.
CSampleDSProperty::~CSampleDSProperty
Standard destructor.
CSampleDSProperty::CreateProperty
Create an ADS Property object, looking up the attribute definitions by calling SampleDSGetPropertyDefinition.
CSampleDSProperty::CreateProperty
Given the attribute definition, create a property object, setting the correspondence between supported ADS syntaxes and the provider syntaxes.
CSampleDSProperty::AllocatePropertyObject
Create a property object and load its type information.
CSampleDSProperty:: QueryInterface
Return the requested interface pointer if available.
CSampleDSProperty:: Standard IADs methods.
CSampleDSProperty:: Standard IADsProperty methods.
MapSyntaxIdtoADsSyntax
Set the correspondence between the syntax id and the ADS syntax.
MapSyntaxIdtoSampleDSSyntax
Set the correspondence between the syntax id and the provider syntax.


CPROPS.CPP

In the example provider component, an example of a property cache implementation can be found in cprops.cpp.

CPropertyCache::addproperty
Extend the property cache by adding a new one.
CPropertyCache::updateproperty
Look up the property, free its contents, and use new values instead; then mark the cache changed for this property.
CPropertyCache::findproperty
Look up this property by name; save its index.
CPropertyCache::getproperty
Find the property in the cache if available, otherwise call GetInfo. Set the index and copy these new values in.
CPropertyCache::putproperty
Find the property. Free what was there, and put new values in their place.
CPropertyCache::CPropertyCache
Standard constructor.
CPropertyCache::~CPropertyCache
Standard destructor.
CPropertyCache::createpropertycache
Create the cache.
CPropertyCache::unboundgetproperty
Find the property in the cache and set it to these values.
CPropertyCache::SampleDSMarshallProperties
Marshal property information and values.
CPropertyCache::marshallproperty
Marshal a property.
CPropertyCache::SampleDSUnMarshallProperties
Unmarshal property information and values.
CPropertyCache::unmarshallproperty
Unmarshal a property.


CPROV.CPP

In the example provider component, one way to implement the ADs provider object is found in cprov.cpp.

CSampleDSProvider::CSampleDSProvider
Standard creator.
CSampleDSProvider::~CSampleDSProvider
Standard destructor.
CSampleDSProvider::Create
Create the object.
CSampleDSProvider::QueryInterface
Check for supported interfaces.
CSampleDSProvider::ParseDisplayName
Resolve the path name.
CSampleDSProvider::ResolvePathName
Get the object and create a pointer moniker for it.


CPROVCF.CPP

In the example provider component, one example of the ADs provider object class factory code is in cprovcf.cpp. The provider component never directly creates an instance of this object at any time other than when the object is created automatically during the binding operations in ADsGetObject or the internal function in the Visual Basic method GetObject.

CSampleDSProviderCF::CreateInstance
Creates an instance of the class factory for the ADs provider object.


CSCHOBJ.CPP

In the example provider component, code managing the lifetime of the schema objects is in cschobj.cpp.

CSampleDSSchema::CreateSchema
Standard constructor.
CSampleDSSchema::~CSampleDSSchema
Standard destructor.
CSampleDSSchema:: Standard IADs methods.
CSampleDSSchema:: Standard IADsContainer methods.
CSampleDSSchema::QueryInterface
Check for the given IID on this object.
CSampleDSSchema::AllocateSchema
Create a schema object and load its type information.


GETOBJ.CPP

In the example provider component, the code that finds and binds to objects is in getobj.cpp.

RelativeGetObject
Gets an object relative to a given ADsPath.
GetObject
Calls ADsObject (parse.cpp) to check the path syntax, validates that the path name has the right provider token and validate the object type. If there are no errors, Create an instance of the correct type of object, and retrieve a pointer to the object's the IUnknown interface.
BuildADsPathFromDSPath
Built an ADsPath string from the native directory path.
BuildDSTreeNameFromADsPath
Use the ADsPath to create a possible tree directory path for the native directory path.
BuildDSPathFromADsPath
Uses ADsPath and DSPathName.
BuildADsParentPath
Build the ADsPath to the parent for this object.
GetNamespaceObject
Validate and CoCreateInstance a example namespace object.
ValidateNamespaceObject
Make sure the namespace object matches the current provider name.
ValidateProvider
Validate provider name (case sensitive)
GetSchemaObject
Validate and open the appropriate schema object type. Then create the right one, and retrieve the IUnknown interface pointer on it.
ValidateSchemaObject
Check that it is a valid schema object type.
ValidateObjectType
Make sure the object type exists in the schema.
BuildSampleDSRootRDNFromADsPath
Build the ADsPath to the root node for the example provider component.
BuildDSPathFromADsPath
Uses ADsPath, DSRootRDN, and DSPathName.


GLOBALS.CPP

In the example provider component, the globals used can be found in globals.cpp.

g_szProviderName
"Sample"
KeywordList
"user", "group", "organizational unit"
Syntax mappings from Sample DS to ADS Variant
Maps between the "String" syntax and the VARIANT VT_BSTR, and "Integer" and VT_I4. This represents the native data format mapping to the Automation-supported formats and vice versa.


GUID.CPP

In the example provider component, example code for. the guids needed for the example provider component objects is in guid.cpp. Guids are defined for the following items:

CLSID_SampleDSNamespace

CLSID_SampleDSProvider

LIBID_SampleDSOle

CLSID_SampleDSGenObject

CLSID_SampleDSSchema

CLSID_SampleDSClass

CLSID_SampleDSProperty


LIBMAIN.CPP

In the example provider component, an example of the standard DLL entry point for adssmp.dll is in libmain.cpp.

DllGetClassObject
Standard DLL entry point for locating class factories.
DllCanUnloadNow
Standard DLL entry point to determine if DLL can be unloaded.
LibMain
Standard DLL initialization entry point.
IsCompatibleOleVersion
Check compatibility with OLE run time.
DllMain
Entry point for most Windows NT®/Windows® 2000 versions.


MEMORY.CPP

In the example provider component, an example showing memory allocation and freeing is in memory.cpp. \

AllocProvMem
Allocates specified memory.
FreeProvMem
Frees memory indicated.
ReallocProvMem
Allocates contiguous memory.
AllocProvStr
Allocates an LPWSTR string.
FreeProvStr
Frees string if not already freed.
ReallocProvStr
Allocates contiguous memory.
ProvAllocString
Checks string and first parameter. If OK, then does allocation.


OBJECT.CPP

In the example provider component, an example of object lists, including using a filter, is in object.cpp.

ObjectTypeList::ObjectTypeList
Creator: Build a list using the filter.
ObjectTypeList::~ObjectTypeList
Standard destructor.
ObjectTypeList::GetCurrentObject
Retrieve the current object from the list.
ObjectTypeList::Next
Bump the index.
ObjectTypeList::Reset
Reset the index to the beginning.
IsValidFilter
Check the integrity of the filter
BuildDefaultObjectArray
Build an array of objects using the filter.
BuildObjectArray
Build an object array.


PROPERTY.CPP

In the example provider component, examples of storing different data types in VARIANT data types so that they can be saved as properties of an object is in property.cpp.

get/put_BSTR_Property
For string properties.
get/put_LONG_Property
For long integer properties.
get/put_DATE_Property
For date format properties.
get/put_VARIANT_BOOL_Property
For boolean properties.
get/put_VARIANT_Property
For variant properties.


PARSE.CPP

In the example provider component, an example of the directory service path name parser is in parse.cpp. The path name parser is a key component in ADs provider components. It checks the syntactic validity of an ADs path name passed in to this provider. If the syntax is valid, an OBJECTINFO structure is constructed, which contains a componentized version of the ADs pathname for this object.

Note that all that is being done is a syntax check. Rather than special-case every nuance new to path names, all path checking must conform to the grammar rules laid out by the parser.

Called from GetObjectW in getobj.cpp.

ADsObject
Parses the ADs pathname passed to it. This function follows the following grammar rules:

<ADsObject> -> <ProviderName> <SampleDSObject>

SampleDSObject
Parses the following grammar rules:

<SampleDSObject> -> "\\" <identifier> "\" <SampleDSObject>

ProviderName
Adds in the syntactically correct provider name if not there.
DsPathName
Parses the following grammar rules:

<Pathname> -> <Component> "\\" <Pathname> OR

<Pathname> -> <Component>

Component
Parses the following grammar rules:

<Identifier> OR

<Identifier> "=" <Identifier>

CLexer::CLexer
Standard constructor.
CLexer::~CLexer
Standard destructor.
CLexer::GetNextToken
Tokenizer.
CLexer::NextChar
Retrieves next single character.
CLexer::PushBackToken
Backs up to beginning of last token.
CLexer::PushbackChar
Backs up one character.
CLexer::IsKeyword
Checks keyword list (defined in globals.h).
CLexer::AddComponent
Adds this component to the component array.
AddProviderName
Adds a syntactically correct provider name into the string to be parsed.
AddRootRDN
Adds the syntactically correct root relative distinguished name (RDN) ]name into the string to be parsed


PACK.CPP

In the example provider component, an example of packing and unpacking data types into VARIANTs is found in pack.cpp. These functions are used when properties are being loaded to and flushed from the property cache and are self-explanatory.

PackStringinVariant/UnpackStringinVariant

PackLONGinVariant/UnpackLONGfromVariant

PackDATEinVariant/UnpackDATEinVariant

PackVARIANT_BOOLinVariant/UnpackVARIANT_BOOLfromVariant

PackVARIANTinVariant/UnpackVARIANTfromVariant


REGDSAPI.CPP

In the example provider component, the routines that represent an API that directly accesses the native operating system are in regdsapi.cpp. The example provider component implements its directory service in the registry. To write a provider that accesses your own directory service, create your own implementations of this API.

SampleDSOpenObject
Open this object by name. If the schema class type parameter is NULL, fill in the type found. Retrieve a handle on the object.
SampleDSCloseObject
Use the handle retrieved by SampleDSOpenObject.
SampleDSRDNEnum
Retrieve the handle on an enumerator object to manage the enumeration of relative distinguished names (RDNs) from a container object.
SampleDSNextRDN
Using the handle retrieved by SampleDSRDNEnum, get the next relative distinguished name from this container object.
SampleDSFreeEnum
Free the enumerator object allocated in SampleDSRDNEnum.
SampleDSModifyObject
Modify properties of an object in the directory service given the handle of the object and a list of attributes and their values.
SampleDSReadObject
Read the properties of the object from the directory service. Map the syntax from the native storage to the appropriate ADS syntax values. Handle properties with multiple values accordingly.
SampleDSGetPropertyDefinition
In the schema, look up all property definitions and their attributes for this type of schema class object.
SampleDSGetPropertyDefinition
In the schema, look up this property and its attributes by name.
SampleDSFreePropertyDefinition
Free memory allocated by GetPropertyDefinition.
SampleDSGetTypeText
Get the schema class type of an object in text format.
SampleDSGetType
Get the schema class type of an object.
SampleDSGetPropertyInfo
Given a handle on the schema class object and a property name, retrieve the property information, like syntax, and so on.
FreeList
Free the memory used by a LPWSTR_LIST.
SampleDSGetClassDefinition
Retrieve the set of all schema class definitions and their associated information from the schema.
SampleDSGetClassDefinition
Retrieve information about a particular schema class in the schema.
SampleDSGetClassInfo
Given the name of a schema class, look up its associated information, like mandatory properties.
SampleDSAddObject
Add an object in the directory service.
SampleDSRemoveObject
Remove an object from the directory service.
SampleDSCreateBuffer
Create a memory buffer for attribute information and operation data.
SampleDSFreeBuffer
Free the buffer created in SampleDSCreateBuffer.


SMPOPER.CPP

In the example provider component, an example showing data type operation routines is in smpoper.cpp.

MarshallSampleDSSynIdToSampleDS
Marshaling example from native to ADS format, using next routine.
CopySampleDSSynIdToSampleDS
Calls routines indented in the following list, one for each syntax.
CopySampleDSSynId1ToSampleDS1
Copies syntax 1 items (strings).
CopySampleDSSynId2ToSampleDS2
Copies syntax 2 items (integers).
UnMarshallSampleDSTOSampleDSSynId
Allocates and copies one or more objects of the same syntax type and value using the next routine.
CopySampleDSToSampleDSSynID
Calls routines indented in the following list, one for each syntax.
CopySampleDS1ToSampleDSSynID1
Copies syntax 1 items (strings).
CopySampleDS2ToSampleDSSynID2
Copies syntax 2 items (integers).
SampleDSTypeCopyConstruct
Makes more than one appropriate copy of the given object, calling the next routine.
SampleDSTypeCopy
Makes a copy of the appropriate type of object, using the indented routines in the follosing list, one for each syntax.
SampleDSTypeCopySampleDSSynId1
Copies a SampleDS_Type_1 object, which is the "string" syntax.
SampleDSTypeCopySampleDSSynId2
Copies a SampleDS_Type_2 object, which is the "Integer" syntax.
SampleDSTypeToVarTypeCopyConstruct
Converting from native to ADS format, using next routine.
SampleDSTypeToVarTypeCopy
Calls routines in the following list, one for each syntax supported.
SampleDSTypeToVarTypeCopySampleDSSynId1
Copies an example provider component DNString to a VT_BSTR format.
SampleDSTypeToVarTypeCopySampleDSSynId2
Copies an example provider component Integer to a VT_I4 format.
VarTypeToSampleDSTypeCopyConstruct
Copies one or more objects calling the next routine.
VarTypeToSampleDSTypeCopy
Copies source objects to destination objects according to Example provider component syntax types. Calls the routines in the following list, one for each syntax.
VarTypeToSampleDSTypeCopySampleDSSynId1
Copies a VT_BSTR to an example provider component DNString format.
VarTypeToSampleDSTypeCopySampleDSSynId2
Copies a VT_I4 to an example provider component integer format.
SampleDSTypeFreeSampleDSObjects
Free memory for one or more objects calling the next routine.
SampleDSTypeClear
Free memory for an example object, calling indented routines in the following list, one per syntax type.
SampleDSTypeFreeSampleDSSynId1
Free memory used for syntax 1 items (DNstrings).
SampleDSTypeFreeSampleDSSynId2
Free memory used for syntax 2 items (Integers).


STDFACT.CPP

In the example provider component, an example showing the standard IClassFactory implementation is in stdfact.cpp. See IClassFactory in the Platform SDK.


ADSI Data Types and Constants

  • ADSI Simple Data Types
  • ADSI Constants


ADSI Simple Data Types

Active Directory™ Service Interfaces (ADSI) defines and uses the following simple data types.

ADSI Data Type | Description
ADS_BOOLEAN | DWORD
PADS_BOOLEAN | Pointer to an ADS_BOOLEAN
ADS_CASE_EXACT_STRING | LPWSTR
PADS_CASE_EXACT_STRING | Pointer to an ADS_CASE_EXACT_STRING
ADS_CASE_IGNORE_STRING | LPWSTR
PADS_CASE_IGNORE_STRING | Pointer to an ADS_CASE_IGNORE_STRING
ADS_DN_STRING | LPWSTR
PADS_DN_STRING | Pointer to an ADS_DN_STRING
ADS_INTEGER | DWORD
PADS_INTEGER | Pointer to an ADS_INTEGER
ADS_LARGE_INTEGER | LARGE_INTEGER
PADS_LARGE_INTEGER; | Pointer to a LARGE_INTEGER
ADS_NUMERIC_STRING | LPWSTR
PADS_NUMERIC_STRING | Pointer to an ADS_NUMERIC_STRING
ADS_OBJECT_CLASS | LPWSTR
PADS_OBJECT_CLASS | Pointer to an ADS_OBJECT_CLASS
ADS_PRINTABLE_STRING | LPWSTR
PADS_PRINTABLE_STRING | Pointer to an ADS_PRINTABLE_STRING
ADS_SEARCH_HANDLE | HANDLE
PADS_SEARCH_HANDLE | Pointer to an ADS_SEARCH_HANDLE
ADS_UTC_TIME | SYSTEMTIME
PADS_UTC_TIME | Pointer to a SYSTEMTIME


ADSI Constants

Active Directory Service Interfaces (ADSI) defines and uses the following constants.

ADSI Constant | Value | Description
ADS_ATTR_APPEND | 3 | Instructs the directory service to append the new value(s) to the existing attribute(s). The attribute and this constant are specified in an ADS_ATTR_INFO array.
ADS_ATTR_CLEAR | 1 | Instructs the directory service to remove attribute value(s) from an object. The attribute and this constant are specified in an ADS_ATTR_INFO array.
ADS_ATTR_DELETE | 4 | Instructs the directory service to delete the named attribute value(s) as specified in an ADS_ATTR_INFO array.
ADS_ATTR_UPDATE | 2 | Instructs the directory service to update the named attribute value(s) as specified in an ADS_ATTR_INFO array.
ADS_EXT_INITCREDENTIALS | 1 | A control code that indicates that the custom data supplied to the IADsExtension::Operate method contains user credentials.
ADS_EXT_INITIALIZE_COMPLETE | 2 | A control code used with IADsExtension::Operate to indicate that extensions can perform any necessary initialization, depending on the functionality supported by the parent object.
ADS_EXT_MAXEXTDISPID | 16777215 | The highest DISPID an extension object can use for its methods and properties.
ADS_EXT_MINEXTDISPID | 1 | The lowest DISPID an extension object can use for its methods and properties.
DBPROPFLAGS_ADSISEARCH | 0x0000C000 | Used to work with the OLE DB provider.

Remarks

The ADS_ATTR_* constants are used by non-Automation clients in C/C++. Specifically, the IDirectoryObject::SetObjectAttributes method uses these constants to specify operations to be performed on given attributes. On the other hand, the ADS_PROPERTY_* constants, as defined in the ADS_PROPERTY_OPERATION_ENUM enumeration, are used by Automation clients in, for example, Visual Basic® or Visual Basic® Scripting Edition (VBScript).

Specifying ADS_ATTR_UPDATE triggers a server side operation that can be computationally intensive. An example would be to initiate the operation to update a long list of group membership. In general, you should refrain from using this operation unless the modification involves a small number of attributes in the directory. To modify a long list of group memberships, the more efficient approach would be to read the list from the underlying directory, make modifications, and store the updated list back to the directory.


ADSI Structures

Active Directory™ Service Interfaces (ADSI) define and use the following structures.

Structure | Description
ADS_ATTR_DEF | Describes a set of attribute definitions of an attributeSchema object.
ADS_ATTR_INFO | Holds the value of a named attribute and specifies operations to modify the attribute.
ADS_NT_SECURITY_DESCRIPTOR | Describes a security descriptor of Windows NT®/Windows® 2000.
ADS_OBJECT_INFO | Describes information about a directory service object.
ADS_PROV_SPECIFIC | Describes a provider-specific data type.
ADS_SEARCH_COLUMN | Represents a column that is the result of a directory search/query.
ADS_SEARCHPREF_INFO | Describes query preferences.
ADS_SORTKEY | Describes the sort key used for sorting a query.
ADSVALUE | Describes the value of an ADSI data type.
ADS_CLASS_DEF | Describes schema information for an object.
ADS_BACKLINK | Represents an ADSI representation of the Back Link syntax used in NDS.
ADS_CASEIGNORE_LIST | Represents an ADSI representation of the Case Ignore List syntax.
ADS_OCTET_LIST | Represents an ADSI representation of the Octet List syntax.
ADS_OCTET_STRING | Represents an ADSI representation of the Octet String syntax.
ADS_PATH | Represents an ADSI representation of the Path syntax used in NDS.
ADS_POSTALADDRESS | Represents an ADSI representation of the Postal Address syntax used in NDS.
ADS_TIMESTAMP | Represents an ADSI representation of the Timestamp syntax used in NDS.
ADS_TYPEDNAME | Represents an ADSI representation of the Typed Name syntax used in NDS.
ADS_HOLD | Represents an ADSI representation of the Hold syntax used in NDS.
ADS_NETADDRESS | Represents an ADSI representation of the Net Address syntax used in NDS.
ADS_REPLICAPOINTER | Represents an ADSI representation of the Replica Pointer syntax used in NDS.
ADS_FAXNUMBER | Represents an ADSI representation of the Facsimile Telephone Number syntax used in NDS.
ADS_EMAIL | Represents an ADSI representation of the EMail syntax used in NDS.


ADS_ATTR_DEF

The ADS_ATTR_DEF structure describes schema information for an attribute. It is used to manage attribute definitions in the schema.

typedef struct _ADS_ATTR_DEF
    {
    LPWSTR  pszAttrName;
    ADSTYPE dwADsType;
    DWORD   dwMinRange;
    DWORD   dwMaxRange;
    BOOL    fMultiValued;
    } ADS_ATTR_DEF, *PADS_ATTR_DEF;

Members

pszAttrName
Pointer to a string that contains the name of the attribute.
dwADsType
Data type of the attribute as defined by ADSTYPE.
dwMinRange
Minimum legal range for this attribute.
dwMaxRange
Maximum legal range for this attribute.
fMultiValued
Whether or not this attribute takes more than one value.

Remarks

In ADSI, attributes and properties are used interchangeably.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_ATTR_INFO

The ADS_ATTR_INFO structure holds the value of a named attribute as well as the information about operations performed on the attribute.

typedef struct _ADS_ATTR_INFO
    {
    LPWSTR    pszAttrName;
    DWORD     dwControlCode;
    ADSTYPE   dwADsType;
    PADSVALUE pADsValues;
    DWORD     dwNumValues;
    } ADS_ATTR_INFO, *PADS_ATTR_INFO;

Members

pszAttrName
Pointer to a string containing the name of the attribute.
dwControlCode
One of the ADS_ATTR_* constants that control what operation to be performed on the attribute. For more information on the ADS_ATTR_* constants, see ADSI Constants.
dwADsType
Data type of the attribute as defined by ADSTYPE.
pADsValues
Pointer to an array of ADSVALUE structures that contain values for the attribute.
dwNumValues
Size of the pADsValues array.

Remarks

In ADSI, attributes and properties are used interchangeably. You can set attributes when creating a directory service object using the IDirectoryObject::CreateDSObject method. The IDirectoryObject interface also supports the IDirectoryObject::GetObjectAttributes and IDirectoryObject::SetObjectAttributes methods for retrieving and modifying the attributes of the object in a directory.

Memory for the array of ADSVALUE structures is not allocated with this structure.

The value of the dwControlCode member is ignored when the structure is used as an OUT parameter.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_BACKLINK

The ADS_BACKLINK structure is an ADSI representation of the Back Link attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_BACKLINK
    {
    DWORD  RemoteID;
    LPWSTR ObjectName;
    } ADS_BACKLINK, *PADS_BACKLINK;

Members

RemoteID
Identifier of the remote server that requires an external reference to the object specified by ObjectName (see below).
ObjectName
The name of an object to which the Back Link attribute is attached.

Remarks

A Back Link attribute contains one or more servers that hold an external reference to the attached object in NDS. For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_CASEIGNORE_LIST

The ADS_CASEIGNORE_LIST structure is an ADSI representation of the Case Ignore List attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct _ADS_CASEIGNORE_LIST
    {
    struct _ADS_CASEIGNORE_LIST __RPC_FAR *Next;
    LPWSTR                                 String;
    } ADS_CASEIGNORE_LIST, *PADS_CASEIGNORE_LIST;

Members

Next
A cursor pointing to the next entry in the list of case-insensitive strings.
String
A string value of the current entry of the list.

Remarks

A Case Ignore List attribute represents an ordered sequence of case insensitive strings. For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_CLASS_DEF

The ADS_CLASS_DEF structure holds the definitions of an object class.

typedef struct  _ADS_CLASS_DEF
    {
    LPWSTR                       pszClassName;
    DWORD                        dwMandatoryAttrs;
    LPWSTR __RPC_FAR            *ppszMandatoryAttrs;
    DWORD                        optionalAttrs;
    LPWSTR __RPC_FAR *__RPC_FAR *ppszOptionalAttrs;
    DWORD                        dwNamingAttrs;
    LPWSTR __RPC_FAR *__RPC_FAR *ppszNamingAttrs;
    DWORD                        dwSuperClasses;
    LPWSTR __RPC_FAR *__RPC_FAR *ppszSuperClasses;
    BOOL                         fIsContainer;
    } ADS_CLASS_DEF, *PADS_CLASS_DEF;

Members

pszClassName
Name of the class.
dwMandatoryAttrs
Number of mandatory attributes of the class.
ppszMandatoryAttrs
Names of the mandatory attributes.
optionalAttrs
Number of optional attributes of the class.
ppszOptionalAttrs
Names of the optional attributes.
dwNamingAttrs
Number of naming attributes.
ppszNamingAttrs
Naming attributes.
dwSuperClasses
Number of super classes of an object of this class.
ppszSuperClasses
Names of the super classes.
fIsContainer
Flags to indicate the object of the class is a container when it is TRUE and not a container when FALSE.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_EMAIL

The ADS_EMAIL structure is an ADSI representation of the EMail Address attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_EMAIL
    {
    LPWSTR Address;
    DWORD  Type;
    } ADS_EMAIL, *PADS_EMAIL;

Members

Address
Email address of the user.
Type
Type of the Email message.

Remarks

For more information about the EMail Address attribute in NDS, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_FAXNUMBER

The ADS_FAXNUMBER structure is an ADSI representation of the Facsimile Telephone Number attribute syntax in Novell NetWare® Directory Services (NDS).

typedef struct ADS_FAXNUMBER
    {
    LPWSTR TelephoneNumber;
    DWORD  NumberOfBits;
    LPBYTE Parameters
    } ADS_FAXNUMBER, *PADS_FAXNUMBER;

Members

TelephoneNumber
The telephone number of the fax machine.
NumberOfBits
Number of data bits.
Parameters
Optional parameters for the fax machine.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_HOLD

The ADS_HOLD structure is an ADSI representation of the Hold attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_HOLD
    {
    LPWSTR ObjectName;
    DWORD  Amount;
    } ADS_HOLD, *PADS_HOLD;

Members

ObjectName
The name of an object that is put on hold.
Amount
Number of charges that a server places against the user on hold while it checks the user account balance.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_NETADDRESS

The ADS_NETADDRESS structure is an ADSI representation of the Net Address attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_NETADDRESS
    {
    DWORD              AddressType;
    DWORD              AddressLength;
    BYTE  __RPC_FAR   *Address;
    } ADS_NETADDRESS, *PADS_NETADDRESS;

Members

AddressType
Types of communication protocols.
AddressLength
Address length in bytes.
Address
A network address used in NDS.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_NT_SECURITY_DESCRIPTOR

The ADS_NT_SECURITY_DESCRIPTOR structure defines the data type of the security descriptor for Windows NT®/Windows® 2000.

typedef struct ADS_NT_SECURITY_DESCRIPTOR
    {
    DWORD  dwLength;
    LPBYTE lpValue;
    } ADS_NT_SECURITY_DESCRIPTOR, *PADS_NT_SECURITY_DESCRIPTOR;

Members

dwLength
The length data, in bytes.
lpValue
Pointer to the security descriptor, represented as a byte array.

Remarks

The ADS_NT_SECURITY_DESCRIPTOR structure is normally used as a member of the ADSVALUE structure definition.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_OBJECT_INFO

The ADS_OBJECT_INFO structure specifies the information, including the identity and location, of a directory service object.

typedef struct _ADS_OBJECT_INFO
    {
    LPWSTR pszRDN;
    LPWSTR pszObjectDN;
    LPWSTR pszParentDN;
    LPWSTR pszSchemaDN;
    LPWSTR pszClassName;
    } ADS_OBJECT_INFO, *PADS_OBJECT_INFO;

Members

pszRDN
Relative distinguished name (relative path) of the directory service object.
pszObjectDN
Distinguished name (path) of the directory service object.
pszParentDN
Distinguished name of the parent object.
pszSchemaDN
Distinguished name of the schema class of the object.
pszClassName
Name of the class of which this object is an instance.

Remarks

To obtain the information of an object, non-Automation clients call the IDirectoryObject::GetObjectInformation method, which takes an out parameter, a pointer to an ADS_OBJECT_INFO structure allocated in the heap. Automation clients can accomplish the same task by calling IADs::GetInfo.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_OCTET_LIST

The ADS_OCTET_LIST structure is an ADSI representation of an ordered sequence of single-byte strings.

typedef struct _ADS_OCTET_LIST
    {
    struct _ADS_OCTET_LIST __RPC_FAR *Next;
    DWORD                             dwLength;
    BYTE __RPC_FAR                   *Data;
    } ADS_OCTET_LIST, *PADS_OCTET_LIST;

Members

Next
Pointer to the next entry in the list.
dwLength
The length, in bytes, of the list.
Data
Byte strings in the list.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_OCTET_STRING

The ADS_OCTET_STRING structure is an ADSI representation of the Octet String attribute syntax used in Active Directory and Novell NetWare® Directory Services (NDS).

typedef struct ADS_OCTET_STRING
    {
    DWORD  dwLength;
    LPBYTE lpValue;
    } ADS_OCTET_STRING, *PADS_OCTET_STRING;

Members

dwLength
The size, in bytes, of the character array.
lpValue
Pointer to an array of single byte characters not interpreted by the underlying directory.

Remarks

Memory for the byte array must be allocated separately.

For more information on the Octet String syntax in NDS, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_PATH

The ADS_PATH structure is an ADSI representation of the Path attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_PATH
    {
    DWORD  Type;
    LPWSTR VolumeName;
    LPWSTR Path;
    } ADS_PATH, *PADS_PATH;

Members

Type
Type of file in the file system.
VolumeName
Name of an existing volume in the file system.
Path
Path of a directory in the file system.

Remarks

The Path attribute in NDS represents a file system path. For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_POSTALADDRESS

The ADS_POSTALADDRESS structure is an ADSI representation of the Postal Address attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct
    {
    LPWSTR PostalAddress[6];
    } ADS_POSTALADDRESS, *PADS_POSTALADDRESS;

Members

PostalAddress
An array of six strings representing the postal address.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_PROV_SPECIFIC

The ADS_PROV_SPECIFIC structure contains provider-specific information represented as a binary blob.

typedef ADS_PROV_SPECIFIC
    {
    DWORD  dwLength;
    LPBYTE lpValue;
    } ADS_PROV_SPECIFIC, *PADS_PROV_SPECIFIC;

Members

dwLength
The size of the character array.
lpValue
Pointer to an array of bytes.

Remarks

The ADS_PROV_SPECIFIC structure is one of the data types used as a member of the ADSVALUE structure definition. The data is represented as a binary blob here, although the actual information can be packed in any data format, such as a C struct. It is the responsibility of the provider writer to publish the specific data format underneath the binary blob.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_REPLICAPOINTER

The ADS_REPLICAPOINTER structure is an ADSI representation of the Replica Pointer attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_REPLICAPOINTER
    {
    LPWSTR          ServerName;
    DWORD           ReplicaType;
    DWORD           ReplicaNumber;
    DWORD           Count;
    PADS_NETADDRESS ReplicaAddressHints;
    } ADS_REPLICAPOINTER, *PADS_REPLICAPOINTER;

Members

ServerName
Name of the name server that holds the replica.
ReplicaType
Type of replica: master, secondary, or read-only.
ReplicaNumber
Replica identification number.
Count
The number of existing replicas
ReplicaAddressHints
A network address that is a likely reference to a node leading to the name server.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SEARCH_COLUMN

The ADS_SEARCH_COLUMN structure specifies the contents of a search column in the query returned from the directory service database.

typedef struct ADS_SEARCH_COLUMN
    {
    LPWSTR    pszAttrName;
    ADSTYPE   dwADsType;
    PADSVALUE pADsValues;
    DWORD     dwNumValues;
    HANDLE    hReserved;
    } ADS_SEARCH_COLUMN, *PADS_SEARCH_COLUMN;

Members

pszAttrName
Pointer to a string that contains the name of the attribute whose values are contained in the current search column.
dwADsType
Value from the ADSTYPE enumeration that indicates how the attribute values are to be interpreted.
pADsValues
Array of ADSVALUE structures that contain values of the attribute in the current search column for the current row.
dwNumValues
Size of the pADsValues array.
hReserved
Reserved for internal use by providers.

Remarks

The ADS_SEARCH_COLUMN structure only contains a pointer to the array of ADSVALUE structures. Memory for the structure must be allocated separately.

To see how ADS_SEARCH_COLUMN is used, see the IDirectorySearch::GetColumn method.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SEARCHPREF_INFO

The ADS_SEARCHPREF_INFO structure specifies the query preferences.

typedef struct ADS_SEARCHPREF_INFO
    {
    ADS_SEARCHPREF dwSearchPref;
    ADSVALUE       vValue;
    ADS_STATUS     dwStatus;
    } ADS_SEARCHPREF_INFO, *PADS_SEARCHPREF_INFO, *LPADS_SEARCHPREF_INFO;

Members

dwSearchPref
One of the ADS_SEARCHPREF enumeration values.
vValue
Value of the search preference that uses the ADSVALUE structure.
dwStatus
One of the ADS_STATUS enumeration values.

Remarks

For examples of how to use the ADS_SEARCHPREF_INFO structure, see the discussions of the IDirectorySearch::SetSearchPreference method and the ADS_SEARCHPREF enumeration.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SORTKEY

The ADS_SORTKEY structure specifies how to sort a query.

typedef struct ADS_SORTKEY
    {
    LPWSTR  pszAttrType;
    LPWSTR  pszReserved;
    BOOLEAN fReverseorder;
    } ADS_SORTKEY, *PADS_SORTKEY;

Members

pszAttrType
Pointer to a stream that contains the type for the attribute.
pszReserved
Reserved for future use.
fReverseorder
Reverse the order of the sorted results.

Remarks

In Active Directory, if TRUE, the fReverseorder parameter specifies that the sort results be ordered from the lowest to the highest.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_TIMESTAMP

The ADS_TIMESTAMP structure is an ADSI representation of the Timestamp attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_TIMESTAMP
    {
    DWORD WholeSeconds;
    DWORD EventID;
    } ADS_TIMESTAMP, *PADS_TIMESTAMP;

Members

WholeSeconds
Number of seconds, with zero value being equal to 12:00 AM, January, 1970, UTC.
EventID
An event identifier, in the order of occurrence, within the period specified by WholeSeconds.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_TYPEDNAME

The ADS_TYPEDNAME structure is an ADSI representation of the Typed Name attribute syntax used in Novell NetWare® Directory Services (NDS).

typedef struct ADS_TYPEDNAME
    {
    LPWSTR ObjectName;
    DWORD  Level;
    DWORD  Interval;
    } ADS_TYPEDNAME, *PADS_TYPEDNAME;

Members

ObjectName
The name of an object.
Level
The priority associated to the object.
Interval
The frequency of reference of the object.

Remarks

For more information, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADSVALUE

The ADSVALUE structure contains a value specified as an ADSI data type. These data types can be ADSI Simple Data Types or ADSI-defined custom data types that include C-structs.

typedef struct ADSVALUE
    {
    ADSTYPE dwType;
    union
        {
        ADS_DN_STRING              DNString;
        ADS_CASE_EXACT_STRING      CaseExactString;
        ADS_CASE_IGNORE_STRING     CaseIgnoreString;
        ADS_PRINTABLE_STRING       PrintableString;
        ADS_NUMERIC_STRING         NumericString;
        ADS_BOOLEAN                Boolean;
        ADS_INTEGER                Integer;
        ADS_OCTET_STRING           OctetString;
        ADS_UTC_TIME               UTCTime;
        ADS_LARGE_INTEGER          LargeInteger;
        ADS_OBJECT_CLASS           ClassName;
        ADS_PROV_SPECIFIC          ProviderSpecific;
        PADS_CASEIGNORE_LIST       pCaseIgnoreList;
        PADS_OCTET_LIST            pOctetList;
        PADS_PATH                  pPath;
        PADS_POSTALADDRESS         pPostalAddress;
        ADS_TIMESTAMP              Timestamp;
        ADS_BACKLINK               BackLink;
        PADS_TYPEDNAME             pTypedName;
        ADS_HOLD                   Hold;
        PADS_NETADDRESS            pNetAddress;
        PADS_REPLICAPOINTER        pReplicaPointer;
        PADS_FAXNUMBER             pFaxNumber;
        ADS_EMAIL                  Email;
        ADS_NT_SECURITY_DESCRIPTOR SecurityDescriptor;
        };
    } ADSVALUE, *PADSVALUE, *LPADSVALUE;

Members

dwType
Data type to be used to interpret the union member of the structure. Values of this member are taken from the ADSTYPE enumeration.
DNString
String identifying the distinguished name (path) of a directory service object, as defined by ADS_DN_STRING, an ADSI simple data type.
CaseExactString
String to be interpreted case-sensitively, as defined by ADS_CASE_EXACT_STRING, an ADSI simple data type.
CaseIgnoreString
String to be interpreted without regard to case, as defined by ADS_CASE_IGNORE_STRING, an ADSI simple data type.
PrintableString
String that can be displayed or printed, as defined by ADS_PRINTABLE_STRING, an ADSI simple data type.
NumericString
Numerals to be interpreted as text, as defined by ADS_NUMERIC_STRING, an ADSI simple data type.
Boolean
Boolean value, as defined by ADS_BOOLEAN, an ADSI simple data type.
Integer
Integer value, as defined by ADS_INTEGER, an ADSI simple data type.
OctetString
An octet string, as defined by ADS_OCTET_STRING, an ADSI-defined data type.
UTCTime
Time specified as Coordinated Universal Time (UTC), as defined by ADS_UTC_TIME, an ADSI simple data type.
LargeInteger
Long integer value, as defined by ADS_LARGE_INTEGER, an ADSI simple data type.
ClassName
Class name string, as defined by ADS_OBJECT_CLASS, an ADSI simple data type.
ProviderSpecific
Provider-specific structure, as defined by ADS_PROV_SPECIFIC, an ADSI-defined data type.
pCaseIgnoreList
Pointer to a list of ADS_CASEIGNORE_LIST, an ADSI-defined data type that is mainly used for the NDS provider.
pOctetList
Pointer to a list of ADS_OCTET_LIST, an ADSI-defined data type that is mainly used for the NDS provider.
pPath
Pointer to the ADS_PATH name, an ADSI-defined data type that is mainly used for the NDS provider.
pPostalAddress
Pointer to the ADS_POSTALADDRESS data, an ADSI-defined data type that is mainly used for the NDS provider.
Timestamp
Time stamp of the ADS_TIMESTAMP type, an ADSI-defined data type that is mainly used for the NDS provider.
BackLink
A link of the ADS_BACKLINK type, an ADSI-defined data type that is mainly used for the NDS provider.
pTypedName
Pointer to the ADS_TYPEDNAME name, an ADSI-defined data type that is mainly used for the NDS provider.
Hold
A data structure of the ADS_HOLD type, an ADSI-defined data type that is mainly used for the NDS provider.
pNetAddress
Pointer to the ADS_NETADDRESS data, an ADSI-defined data type that is mainly used for the NDS provider.
pReplicaPointer
Pointer to a replica pointer of ADS_REPLICAPOINTER, an ADSI-defined data type that is mainly used for the NDS provider.
pFaxNumber
Pointer to a fax number of ADS_FAXNUMBER, an ADSI-defined data type that is mainly used for the NDS provider.
Email
Email address of a user of ADS_EMAIL, an ADSI-defined data type that is mainly used for the NDS provider.
SecurityDescriptor
Windows NT/Windows 2000 security descriptor, as defined by ADS_NT_SECURITY_DESCRIPTOR, an ADSI-defined data type.

Remarks

Members of the ADSVALUE structure are used to specify the data type of attributes. For example, see ADS_ATTR_INFO.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADSI Enumerations

Active Directory™ Service Interfaces (ADSI) define and use the following enumerations.

Enumeration | Description
ADS_ACEFLAG_ENUM | Specifies how security propagates for inherited access-control entries (ACEs) and types of auditing for a system ACE.
ADS_ACETYPE_ENUM | Specifies the type of ACE.
ADS_AUTHENTICATION_ENUM | Specifies the security levels used in authenticating a client.
ADS_CHASE_REFERRALS_ENUM | Specifies the behavior of referral chasing.
ADS_DEREFENUM | Specifies the behavior of alias dereferencing.
ADS_DISPLAY_ENUM | Specifies how a path name is to be displayed.
ADS_FLAGTYPE_ENUM | Specifies the presence of the ObjectType or InheritedObjectType fields in an ACE.
ADS_FORMAT | Specifies the type of values in a pathname object.
ADS_GROUP_TYPE_ENUM | Specifies the group type of the member.
ADS_OPTION_ENUM | Specifies the available options that the IADsObjectOptions interface uses for manipulating directory objects.
ADS_PREFERENCES_ENUM | Specifies the query preferences of the OLE DB for ADSI.
ADS_PROPERTY_OPERATION_ENUM | Specifies the ways to update property values in the property cache.
ADS_RIGHTS_ENUM | Specifies the access rights to a directory service object.
ADS_SYSTEMFLAG_ENUM | Specifies the types of attributes represented by an attributeSchema object.
ADS_SCOPEENUM | Specifies the scope of a directory search.
ADS_SD_CONTROL_ENUM | Specifies that an access-control list (ACL) is to be protected when new permissions are recursively applied to a directory tree.
ADS_SD_REVISION_ENUM | Specifies the revision number of an ACE or ACL.
ADS_SEARCHPREF | Specifies preferences of the search.
ADS_SECURITY_INFO_ENUM | Specifies the options for examining security information.
ADS_SETTYPE | Specifies the pathname format in IADsPathname::Set.
ADS_STATUS | Specifies the status of search preferences.
ADS_USER_FLAG | Specifies flags used for manipulating user properties.
ADSI_DIALECT_ENUM | Specifies available ADSI query dialects.
ADSTYPE | Specifies data types used to interpret an ADSI extended syntax string.

Remarks

Since VBS can not read information from a type library, VBS applications do not understand the symbolic constants as defined in these enumerations. A VBS script should use the numerical constants, instead, to set the appropriate flags.


ADS_ACEFLAG_ENUM

The ADS_ACEFLAG_ENUM enumeration specifies how security may propagate along an inheritance path and how the system generates audit messages.

enum {
    ADS_ACEFLAG_INHERIT_ACE              = 0x2,
    ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE = 0x4,
    ADS_ACEFLAG_INHERIT_ONLY_ACE         = 0x8,
    ADS_ACEFLAG_INHERITED_ACE            = 0x10,
    ADS_ACEFLAG_VALID_INHERIT_FLAGS      = 0x1f,
    ADS_ACEFLAG_SUCCESSFUL_ACCESS        = 0x40,
    ADS_ACEFLAG_FAILED_ACCESS            = 0x80
    } ADS_ACEFLAG_ENUM;

Elements

ADS_ACEFLAG_INHERIT_ACE
Child objects will inherit this access-control entry (ACE). The inherited ACE is inheritable unless the ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE flag is set.
ADS_ACEFLAG_NO_PROPAGATE_INHERIT_ACE
ADSI will clear the ADS_ACEFLAG_INHERIT_ACE flag for the inherited ACEs of child objects. This prevents the ACE from being inherited by subsequent generations of objects.
ADS_ACEFLAG_INHERIT_ONLY_ACE
Indicates an inherit-only ACE that does not exercise access control on the object to which it is attached. If this flag is not set, the ACE is an effective ACE that exerts access control on the object to which it is attached.
ADS_ACEFLAG_INHERITED_ACE
Indicates whether or not the ACE was inherited. The system sets this bit.
ADS_ACEFLAG_VALID_INHERIT_FLAGS
Indicates whether the inherit flags are valid. The system sets this bit.
ADS_ACEFLAG_SUCCESSFUL_ACCESS
Generates audit messages for successful access attempts, used with ACEs that audit the system in a system access-control list (SACL).
ADS_ACEFLAG_FAILED_ACCESS
Generates audit messages for failed access attempts, used with ACEs that audit the system in a SACL.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_ACETYPE_ENUM

The ADS_ACETYPE_ENUM enumeration specifies types of access-control entries (ACEs).

enum {
    ADS_ACETYPE_ACCESS_ALLOWED           = 0,
    ADS_ACETYPE_ACCESS_DENIED            = 0x1,
    ADS_ACETYPE_SYSTEM_AUDIT             = 0x2,
    ADS_ACETYPE_ACCESS_ALLOWED_OBJECT    = 0x5,
    ADS_ACETYPE_ACCESS_DENIED_OBJECT     = 0x6,
    ADS_ACETYPE_SYSTEM_AUDIT_OBJECT      = 0x7
    } ADS_ACETYPE_ENUM;

Elements

ADS_ACETYPE_ACCESS_ALLOWED
The ACE is of the standard ACCESS ALLOWED type, where the ObjectType and InheritedOjectType fields are NULL.
ADS_ACETYPE_ACCESS_DENIED
The ACE is of the standard ACCESS_DENIED type, where the ObjectType and InheritedObjectType fields are NULL.
ADS_ACETYPE_SYSTEM_AUDIT
The ACE is of the standard system type, where the ObjectType and InheritedObjectType fields are NULL.
ADS_ACETYPE_ACCESS_ALLOWED_OBJECT
The ACE is of the ADSI extension of the ACCESS ALLOWED type, where either ObjectType or InheritedObjectType or both contain a GUID.
ADS_ACETYPE_ACCESS_DENIED_OBJECT
The ACE is of the ADSI extension of the ACCCESS_DENIED type, where either ObjectType or InheritedObjectType or both contain a GUID.
ADS_ACETYPE_SYSTEM_AUDIT_OBJECT
The ACE is of the ADSI extension of the system type, where either ObjectType or InheritedObjectType or both contain a GUID.

Remarks

A standard ACE is the one defined and used in a Windows NT®/Windows® 2000 security descriptor. ADSI extension allows the ACE to be applied to objects and properties identified by GUIDs.

Use the IADsAccessControlEntry property method to determine the ACE type.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_AUTHENTICATION_ENUM

The ADS_AUTHENTICATION enumeration specifies authentication options used in ADSI for binding to directory service objects. When calling IADsOpenDSObject or ADsOpenObject to bind to an ADSI object, you must supply at least one of the options. In general, different providers will have different implementations. The options documented here apply to the providers supplied by Microsoft® that are shipped with the ADSI SDK. For more information, see ADSI System Providers.

typedef enum {
    ADS_SECURE_AUTHENTICATION  = 0x1,
    ADS_USE_ENCRYPTION         = 0x2,
    ADS_USE_SSL                = 0x2,
    ADS_READONLY_SERVER        = 0x4,
    ADS_PROMPT_CREDENTIALS     = 0x8,
    ADS_NO_AUTHENTICATION      = 0x10,
    ADS_FAST_BIND              = 0x20,
    ADS_USE_SIGNING            = 0x40,
    ADS_USE_SEALING            = 0x80
    } ADS_AUTHENTICATION_ENUM;

Elements

ADS_SECURE_AUTHENTICATION
Requests secure authentication. When this flag is set, the WinNT provider uses NT LAN Manager (NTLM) to authenticate the client. Active Directory will use Kerberos, and possibly NTLM, to authenticate the client. When the user name and password are NULL, ADSI uses the credentials of the currently logged-on user.
ADS_USE_ENCRYPTION
Forces ADSI to use encryption for data exchange over the network.
ADS_USE_SSL
Encrypts the channel with SSL. Data will be encrypted using SSL. Active Directory requires that the Certificate Server be installed to support SSL encryption.
ADS_READONLY_SERVER
For a WinNT provider, ADSI tries to connect to a primary domain controller (PDC) or a backup domain controller (BDC). For Active Directory, this flag indicates that a writeable server is not required for a serverless binding.
ADS_PROMPT_CREDENTIALS
User credentials are prompted when the authentication is initiated, if the selected Security Support Provider Interface (SSPI) provides a user interface to do so.
ADS_NO_AUTHENTICATION
Request no authentication. The providers may attempt to bind client, as an anonymous user, to the targeted object. The WinNT provider does not support this flag. Active Directory establishes a connection between the client and the targeted object, but will not perform any authentication. Setting this flag amounts to requesting an anonymous binding, which means "Everyone" as the security context.
ADS_FAST_BIND
When this flag is set, ADSI will not attempt to query the objectClass property and thus will only expose the base interfaces supported by all ADSI objects instead of the full object support. A user can use this option to boost the performance in a series of object manipulations that involve only methods of the base interfaces. However, ADSI will not verify if any of the request objects actually exist on the server. For more information, see "Fast Binding Options for Batch Write/Modify Operations" in Active Directory Programmer's Guide.
ADS_USE_SIGNING
Verifies data integrity to ensure the data received is the same as the data sent. The ADS_SECURE_AUTHENTICATION flag must be set also in order to use the signing.
ADS_USE_SEALING
Encrypts data using Kerberos. The ADS_SECURE_AUTHENTICATION flag must be set also in order to use the sealing.

Remarks

Serverless binding refers to a process in which a client attempts to bind to an Active Directory object without explicitly specifying an Active Directory server in the binding string, for example, "LDAP://CN=jsmith,DC=ArcadiaBay,DC=Com". This is possible because the LDAP provider relies on the locator services of Windows® 2000 to find the best domain controller (DC) for the client. However, the client must have an account on the Active Directory domain controller in order to take advantage of the serverless binding feature.

Example Code [Visual Basic]

The following Visual Basic® code snippet illustrates how to use IADsOpenDSObject to open the "Administrator" user object on "ArcadiaBay" with secure authentication for the WinNT provider.

Dim dso As IADsOpenDSObject
Dim domain As IADsDomain

Set dso = GetObject("WinNT:")
Set domain = dso.OpenDSObject("WinNT://ArcadiaBay", "Administrator", "secret", ADS_SECURE_AUTHENTICATION)

Example Code [C++]

The following C/C++ code snippet illustrates how the ADS_SECURE_AUTHENTICATION flag is used with ADsOpenObject for validating the user bound as "JSmith".

IADs    *pObject;
HRESULT hr;
hr = ADsOpenObject(L"LDAP://CN=Jsmith, DC=ArcadiaBay, DC=com",
                   L"ArcadiaBay\\JSmith", L"password",
                   ADS_SECURE_AUTHENTICATION, IID_IADs,
                   (void**) &pObject);

The user name can be of the UPN format: "JSmith@ArcadiaBay.com", as well as the distinguished name format: "CN=JSmith,DC=ArcadiaBay,DC=COM".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_CHASE_REFERRALS_ENUM

The ADS_CHASE_REFERRALS_ENUM enumeration specifies if and how referral chasing is pursued. When a server determines that other servers hold relevant information, in part or as a whole, it may refer the client to another server to obtain the result. Referral chasing is the action taken by a client to contact the referred-to server to continue the directory search.

typedef enum {
    ADS_CHASE_REFERRALS_NEVER         = 0x00,
    ADS_CHASE_REFERRALS_SUBORDINATE   = 0x20,
    ADS_CHASE_REFERRALS_EXTERNAL      = 0x40,
    ADS_CHASE_REFERRALS_ALWAYS        = 0x20 | 0x40
    } ADS_CHASE_REFERRALS_ENUM

Elements

ADS_CHASE_REFERRALS_NEVER
The client should never chase the referred-to server. Setting this option prevents a client from contacting other servers in a referral process.
ADS_CHASE_REFERRALS_SUBORDINATE
The client chases only subordinate referrals which are a subordinate naming context in a directory tree. For example, if the base search is requested for DC=ArcadiaBay, DC=Com, and the server returns a result set and a referral of DC=Sales, DC=ArcadiaBay, DC=Com on the AdbSales server, the client can contact the AdbSales server to continue the search. The ADSI LDAP provider always turns off this flag for paged searches.
ADS_CHASE_REFERRALS_EXTERNAL
The client chases external referrals. For example, a client requests server A to perform a search for DC=ArcadiaBay,DC=Com. However, server A does not contain the object, but knows that an independent server, B, owns it. It then refers the client to server B.
ADS_CHASE_REFERRALS_ALWAYS
Referrals are chased for either the subordinate or external type.

Remarks

Use the constants of this enumeration to set up search preferences for referral chasing. The action amounts to assigning the appropriate fields of the ADS_SEARCHPREF_INFO structure with elements of the ADS_CHASE_REFERRALS_ENUM and ADS_SEARCHPREF enumerations.

The ADSI LDAP provider supports external referrals for paged searches, but does not support subordinate referrals during paging.

Example Code [C++]

The following code snippet illustrates how to set up search preference for chasing referrals, in a C/C++ application.

///////////
int idx;
ADS_SEARCHPREF_INFO prefInfo;
idx = m_cChaseReferrals.GetCurSel();
if ( idx != CB_ERR )
{
    prefInfo.dwSearchPref = ADS_SEARCHPREF_CHASE_REFERRALS;
    prefInfo.vValue.dwType = ADSTYPE_INTEGER;
    switch( idx )
    {
        case 0:
            prefInfo.vValue.Integer = ADS_CHASE_REFERRALS_NEVER;
            break;
        case 1:
            prefInfo.vValue.Integer = ADS_CHASE_REFERRALS_SUBORDINATE;
            break;
        case 2:
            prefInfo.vValue.Integer = ADS_CHASE_REFERRALS_EXTERNAL;
            break;
        default:
            prefInfo.vValue.Integer = ADS_CHASE_REFERRALS_EXTERNAL;
    }
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_DEREFENUM

The ADS_DEREFENUM enumeration specifies the behavior in which aliases are dereferenced.

enum {
    ADS_DEREF_NEVER        = 0,
    ADS_DEREF_SEARCHING    = 1,
    ADS_DEREF_FINDING      = 2,
    ADS_DEREF_ALWAYS       = 3
    } ADS_DEREFENUM;

Elements

ADS_DEREF_NEVER
Does not dereference aliases when searching or locating the base object of the search.
ADS_DEREF_SEARCHING
Dereferences aliases when searching subordinates of the base object, but not when locating the base itself.
ADS_DEREF_FINDING
Dereferences aliases when locating the base object of the search, but not when searching its subordinates.
ADS_DEREF_ALWAYS
Dereferences aliases when both searching subordinates and locating the base object of the search.

Remarks

The IDirectorySearch interface uses these constants to set the alias dereferencing behavior. If no option is specified, the server determines the default behavior.

Example Code [C++]

The following C++ code snippet illustrates how to set the search preference for alias dereferencing. m_pSearch refers to a pointer to an object implementing the IDirectorySearch interface.

ADS_SEARCHPREF_INFO prefInfo[1];
HRESULT hr;

prefInfo[0] = ADS_SEARCHPREF_DEREF_ALIASES;
prefInfo[0].vValue.dwType = ADSTYPE_INTEGER;
prefInfo[0].vValue.Integer = ADS_DEREF_ALWAYS;
hr = m_pSearch->SetSearchPreference(prefInfo, 1);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_DISPLAY_ENUM

The ADS_DISPLAY_ENUM enumeration specifies how a path name is to be displayed.

enum {
      ADS_DISPLAY_FULL       = 1,
      ADS_DISPLAY_VALUE_ONLY = 2
    } ADS_DISPLAY_ENUM;

Elements

ADS_DISPLAY_FULL
The path name is displayed with both attributes and values. For example, CN=John Doe.
ADS_DISPLAY_VALUE_ONLY
The path name is displayed with values only. For example, John Doe.

Remarks

Use pass to the IADsPathname::SetDisplayType method the display type given above to specify how to display a path name.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_ESCAPE_MODE_ENUM

The ESCAPE_MODE_ENUM enumeration specifies how escaped characters are handled in a path name.

enum {
      ADS_ESCAPEDMODE_DEFAULT  = 1,
      ADS_ESCAPEDMODE_ON       = 2,
      ADS_ESCAPEDMODE_OFF      = 3
    } ADS_ESCAPE_MODE_ENUM;

Elements

ADS_ESCAPEDMODE_DEFAULT
The default mode is ADS_ESCAPEDMODE_ON.
ADS_ESCAPEDMODE_ON
Display escaped characters in the path name string.
ADS_ESCAPEDMODE_OFF
Do not display escaped characters in the path name string.

Remarks

You call the IADsPathname::put_EscapedMode method with one of the options given above to specify how escape characters are handled in a path name. You use the IADsPathname::get_EscapedMode method to examine which mode has been set.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_FLAGTYPE_ENUM

The ADS_FLAGTYPE_ENUM enumeration specifies values that can be used to indicate the presence of the ObjectType or InheritedObjectType fields in the access-control entry (ACE).

enum {
    ADS_FLAG_OBJECT_TYPE_PRESENT             = 0x1,
    ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT   = 0x2
    } ADS_FLAGTYPE_ENUM;

Elements

ADS_FLAG_OBJECT_TYPE_PRESENT
The ObjectType field is present in the ACE, but InheritedObjectType is not.
ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT
The InheritedObjectType field is present in the ACE, but ObjectType is not.

Remarks

ObjectType indicates what object type, property set, or property an ACE refers to. It takes a GUID as its value. The GUID referenced by ObjectType is not physically present in the ACE unless ADS_FLAGS_OBJECT_TYPE_PRESENT is set.

InheritedObjectType specifies the GUID of an object that will inherit the ACE. The GUID is not physically present in the ACE unless the ADS_FLAG_INHERITED_OBJECT_TYPE_PRESENT bit is set.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_FORMAT

The ADS_FORMAT enumeration specifies the available pathname value types used by the IADsPathname::Retrieve method.

typedef enum ADS_FORMAT {
    ADS_FORMAT_WINDOWS           = 1,
    ADS_FORMAT_WINDOWS_NO_SERVER = 2,
    ADS_FORMAT_WINDOWS_DN        = 3,
    ADS_FORMAT_WINDOWS_PARENT    = 4,
    ADS_FORMAT_X500              = 5,
    ADS_FORMAT_X500_NO_SERVER    = 6,
    ADS_FORMAT_X500_DN           = 7,
    ADS_FORMAT_X500_PARENT       = 8,
    ADS_FORMAT_SERVER            = 9,
    ADS_FORMAT_PROVIDER          = 10,
    ADS_FORMAT_NAMESPACE         = 10,
    ADS_FORMAT_LEAF              = 11,
    } ADS_FORMAT, *PADS_FORMAT;

Elements

ADS_FORMAT_WINDOWS
Returns the full path in windows format, for example, "LDAP://servername/o=internet/…/cn=bar".
ADS_FORMAT_WINDOWS_NO_SERVER
Returns Windows format without server, for example, "LDAP://o=internet/…/cn=bar".
ADS_FORMAT_WINDOWS_DN
Returns Windows format of the distinguished name only, for example, "o=internet/…/cn=bar".
ADS_FORMAT_WINDOWS_PARENT
Returns Windows format of Parent only, for example, "o=internet/…".
ADS_FORMAT_X500
Returns the full path in X500 format, for example, "LDAP://servername/cn=bar, …, o=internet".
ADS_FORMAT_X500_NO_SERVER
ADS_FORMAT_X500_DN
Returns only the distinguished name in X500 format. For example, "cn=bar,…,o=internet".
ADS_FORMAT_X500_PARENT
Returns only the parent in X500 format, for example, "…,o=internet".
ADS_FORMAT_SERVER
Returns the server name, for example, "servername".
ADS_FORMAT_PROVIDER
Returns the name of the provider, for example, "LDAP".
ADS_FORMAT_NAMESPACE
The same as ADS_FORMAT_PROVIDER.
ADS_FORMAT_LEAF
Returns the name of the leaf, for example, "cn=bar".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_GROUP_TYPE_ENUM

The ADS_GROUP_TYPE_ENUM enumeration specifies the type of group objects in ADSI.

enum {
    ADS_GROUP_TYPE_GLOBAL_GROUP        = 0x00000002,
    ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP  = 0x00000004,
    ADS_GROUP_TYPE_LOCAL_GROUP         = 0x00000004,
    ADS_GROUP_TYPE_UNIVERSAL_GROUP     = 0x00000008,
    ADS_GROUP_TYPE_SECURITY_ENABLED    = 0x80000000
    } ADS_GROUP_TYPE_ENUM;

Elements

ADS_GROUP_TYPE_GLOBAL_GROUP
Group that contains only accounts and other account groups from its own domain. This group may be exported to a different domain.
ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP
Group that can contain accounts and universal groups from any domains. It may not be included in either access-control lists of resources in other domains or groups other than global groups in the same domain.
ADS_GROUP_TYPE_LOCAL_GROUP
This flag is for the WinNT provider as the ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP flag is for the LDAP provider.
ADS_GROUP_TYPE_UNIVERSAL_GROUP
Group that can contain accounts and account groups from any domains, but not domain local groups.
ADS_GROUP_TYPE_SECURITY_ENABLED
Group that is security enabled. This group can be used to apply an access-control list on an ADSI object or a file system.

Example Code [Visual Basic]

The following code snippet illustrates how you might use elements of this enumeration.

Dim rootDSE As IADs
Dim dom As IADsContainer
Dim grp As IADs

Set rootDSE = GetObject("LDAP://RootDSE")
Set dom = GetObject("LDAP://" & rootDSE.Get("defaultNamingContext"))

'Creating Secured Global Group
Set grp = dom.Create("group", "CN=group1")
grp.Put "samAccountName", "group1"
grp.Put "groupType", ADS_GROUP_TYPE_GLOBAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED
grp.SetInfo

'Creating Distribution List Local Group
Set grp = dom.Create("group", "CN=group2")
grp.Put "samAccountName", "group2"
grp.Put "groupType", ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP
grp.SetInfo

'Create Secured Universal Group (ONLY IN NATIVE MODE)
Set grp = dom.Create("group", "CN=group3")
grp.Put "samAccountName", "group3"
grp.Put "groupType", ADS_GROUP_TYPE_UNIVERSAL_GROUP Or ADS_GROUP_TYPE_SECURITY_ENABLED
grp.SetInfo


ADS_OPTION_ENUM

The ADS_OPTION_ENUM enumeration specifies the available options that the IADsObjectOptions interface gets or sets for manipulating directory objects.

enum {
    ADS_OPTION_SERVERNAME     = 0,
    ADS_OPTION_REFERRALS      = 1,
    ADS_OPTION_PAGE_SIZE      = 2,
    ADS_OPTION_SECURITY_MASK  = 3
    } ADS_OPTION_ENUM;

Elements

ADS_OPTION_SERVERNAME
Specifies that the host names of servers are to be set or obtained in a query.
ADS_OPTION_REFERRALS
Specifies that referral chasing is to be performed in a query.
ADS_OPTION_PAGE_SIZE
Specifies that page sizes are to be set or obtained in a paged search.
ADS_OPTION_SECURITY_MASK
Specifies that security information, including the system access-control list (ACL), of an object is to be examined. This element takes any combination of the bit-masks as defined in the ADS_SECURITY_INFO_ENUM enumeration.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_PREFERENCES_ENUM

The ADS_PREFERENCES enumeration specifies the query preferences of the OLE DB provider for ADSI.

typedef enum {
    ADSIPROP_ASYNCHRONOUS      = 0,
    ADSIPROP_DEREF_ALIASES     = 0x1,
    ADSIPROP_SIZE_LIMIT        = 0x2,
    ADSIPROP_TIME_LIMIT        = 0x3,
    ADSIPROP_ATTRIBTYPES_ONLY  = 0x4,
    ADSIPROP_SEARCH_SCOPE      = 0x5,
    ADSIPROP_TIMEOUT           = 0x6,
    ADSIPROP_PAGESIZE          = 0x7,
    ADSIPROP_PAGED_TIME_LIMIT  = 0x8,
    ADSIPROP_CHASE_REFERRALS   = 0x9,
    ADSIPROP_SORT_ON           = 0xa,
    ADSIPROP_CACHE_RESULTS     = 0xb
    } ADS_PREFERENCES_ENUM;

Elements

ADSIPROP_ASYNCHRONOUS
Requests an asynchronous search.
ADSIPROP_DEREF_ALIASES
Specifies that aliases of found objects are to be resolved. Use ADS_DEREFENUM to specify how this should be done.
ADSIPROP_SIZE_LIMIT
Specifies the size limit that the server should observe in a search. The size limit is the maximum number of returned objects. A zero value indicates that no size limit is imposed. The server stops searching once the size limit is reached and returns the results accumulated up to that point.
ADSIPROP_TIME_LIMIT
Specifies the time limit (in seconds) that the server should observe in a search. A zero value means that no time limit restriction is imposed. When the time limit is reached, the server stops searching and returns whatever results it has accumulated up to that point.
ADSIPROP_ATTRIBTYPES_ONLY
Indicates that the search should obtain only the name of attributes to which values have been assigned.
ADSIPROP_SEARCH_SCOPE
Specifies the search scope that should be observed by the server. For the appropriate settings, see the ADS_SCOPEENUM enumeration.
ADSIPROP_TIMEOUT
Specifies the time limit (in seconds) that a client is willing to wait for the server to return the result.
ADSIPROP_PAGESIZE
Specifies the page size in a paged search. For each request by the client, the server returns, at most, the number of objects as set by the page size.
ADSIPROP_PAGED_TIME_LIMIT
Specifies the time limit (in seconds) that the server should observe to search a page of results (as opposed to the time limit for the entire search).
ADSIPROP_CHASE_REFERRALS
Specifies that referrals may be chased. If the root search is not specified in the naming context of the server or when the search results cross a naming context (for example, when you have child domains and search in the parent domain), the server sends a referral message to the client which the client can choose to ignore or chase. By default, this option is set to ADS_CHASE_REFERRALS_EXTERNAL. For more information on referrals chasing, see ADS_CHASE_REFERRALS_ENUM.
ADSIPROP_SORT_ON
Specifies that the server sorts the result set. Use the ADS_SORTKEY structure to specify the sort keys.
ADSIPROP_CACHE_RESULTS
Specifies if the result should be cached on the client side. By default, ADSI caches the result set. Turning off this option may be more desirable for large result sets.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_PROPERTY_OPERATION_ENUM

The ADS_PROPERTY_OPERATION_ENUM enumeration specifies ways to update a named property in the cache.

enum {
    ADS_PROPERTY_CLEAR       = 1,
    ADS_PROPERTY_UPDATE      = 2,
    ADS_PROPERTY_APPEND      = 3,
    ADS_PROPERTY_DELETE      = 4
    } ADS_PROPERTY_OPERATION_ENUM;

Elements

ADS_PROPERTY_CLEAR
Instructs the directory service to remove the property value(s) from the object.
ADS_PROPERTY_UPDATE
Instructs the directory service to replace the current value with the element(s) in the passed VARIANT array.
ADS_PROPERTY_APPEND
Instructs the directory service to append the new value(s) to the existing one(s).
ADS_PROPERTY_DELETE
Instructs the directory service to delete the specified value(s) of a property.

Remarks

The elements of this enumeration are used with the IADs::PutEx method, the document of which provides an example of how to use these enumerated constants.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_RIGHTS_ENUM

The ADS_RIGHTS_ENUM enumeration specifies the access rights to a directory object. The member values are assigned to the AccessMask field of an ACE.

enum {
    ADS_RIGHT_DELETE                 = 0x10000,
    ADS_RIGHT_READ_CONTROL           = 0x20000,
    ADS_RIGHT_WRITE_DAC              = 0x40000,
    ADS_RIGHT_WRITE_OWNER            = 0x80000,
    ADS_RIGHT_SYNCHRONIZE            = 0x100000,
    ADS_RIGHT_ACCESS_SYSTEM_SECURITY = 0x1000000,
    ADS_RIGHT_GENERIC_READ           = 0x80000000,
    ADS_RIGHT_GENERIC_WRITE          = 0x40000000,
    ADS_RIGHT_GENERIC_EXECUTE        = 0x20000000,
    ADS_RIGHT_GENERIC_ALL            = 0x10000000,
    ADS_RIGHT_DS_CREATE_CHILD        = 0x1,
    ADS_RIGHT_DS_DELETE_CHILD        = 0x2,
    ADS_RIGHT_ACTRL_DS_LIST          = 0x4,
    ADS_RIGHT_DS_SELF                = 0x8,
    ADS_RIGHT_DS_READ_PROP           = 0x10,
    ADS_RIGHT_DS_WRITE_PROP          = 0x20,
    ADS_RIGHT_DS_DELETE_TREE         = 0x40,
    ADS_RIGHT_DS_LIST_OBJECT         = 0x80,
    ADS_RIGHT_DS_CONTROL_ACCESS      = 0x100
    } ADS_RIGHTS_ENUM;

Elements

ADS_RIGHT_DELETE
The right to delete the object.
ADS_RIGHT_READ_CONTROL
The right to read information from the security descriptor of the object, not including the information in the SACL.
ADS_RIGHT_WRITE_DAC
The right to modify the discretionary access-control list (DACL) in the object's security descriptor.
ADS_RIGHT_WRITE_OWNER
The right to assume ownership of the object. The user must be a trustee of the object. The user cannot transfer the ownership to other users.
ADS_RIGHT_SYNCHRONIZE
The right to use the object for synchronization. This enables a thread to wait until the object is in the signaled state.
ADS_RIGHT_ACCESS_SYSTEM_SECURITY
The right to get or set the SACL in the object's security descriptor.
ADS_RIGHT_GENERIC_READ
The right to read from the security descriptor, examine the object as well as its children, and read all properties.
ADS_RIGHT_GENERIC_WRITE
The right to write all the properties and write to the DACL. The user can add and remove the object to and from the directory.
ADS_RIGHT_GENERIC_EXECUTE
The right to list children of this object.
ADS_RIGHT_GENERIC_ALL
The right to create or delete children, delete a subtree, read and write properties, examine children and the object itself, add and remove the object from the directory, and read or write with an extended right.
ADS_RIGHT_DS_CREATE_CHILD
The right to create children of the object. The ObjectType member of an ACE can contain a GUID that identifies the type of child object whose creation is being controlled. If ObjectType does not contain a GUID, the ACE controls the creation of all child object types.
ADS_RIGHT_DS_DELETE_CHILD
The right to delete children of the object. The ObjectType member of an ACE can contain a GUID that identifies a type of child object whose deletion is being controlled. If ObjectType does not contain a GUID, the ACE controls the deletion of all child object types.
ADS_RIGHT_ACTRL_DS_LIST
The right to list children of this object.
ADS_RIGHT_DS_SELF
The right to modify the group membership of a group object.
ADS_RIGHT_DS_READ_PROP
The right to read properties of the object. The ObjectType member of an ACE can contain a GUID that identifies a property set or property. If ObjectType does not contain a GUID, the ACE controls the right to read all of the object's properties.
ADS_RIGHT_DS_WRITE_PROP
The right to write properties of the object. The ObjectType member of an ACE can contain a GUID that identifies a property set or property. If ObjectType does not contain a GUID, the ACE controls the right to write all of the object's properties.
ADS_RIGHT_DS_DELETE_TREE
The right to delete all children of this object, regardless of the permission on the children.
ADS_RIGHT_DS_LIST_OBJECT
The right to list a particular object. If the user is not granted such a right, the object is hidden from the user.
ADS_RIGHT_DS_CONTROL_ACCESS
The right to perform an operation controlled by an extended access right. The ObjectType member of an ACE can contain a GUID that identifies the extended right. If ObjectType does not contain a GUID, the ACE controls the right to perform all extended right operations associated with the object.

Remarks

To assign access rights to an object, set the AccessMask field of an access-control entry (ACE) to a combination of the constants defined in this enumeration. In addition to the AccessMask field, an ACE can have other fields, including ACEType, ACEFlags, ObjectType, InheritedObjectType, Flags, and Trustee. The IADsAccessControlEntry interface provides property methods to obtain and modify these fields.

The ObjectType field specifies a GUID that identifies the property set, property, extended right, or type of child object to which the ACE applies. The InheritedObjectType field specifies a GUID that identifies the type of child object that can inherit the ACE. The Trustee field identifies the security principal to whom the ACE allows or denies the specified access rights.

For information on ACEType, ACEFlags, and Flags, see ADS_ACETYPE_ENUM, ADS_ACEFLAGS_ENUM.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SYSTEMFLAG_ENUM

The ADS_SYSTEMFLAG_ENUM enumeration defines the values that can be assigned to the systemflags attribute of the attributeSchema object, as well as other objects, such as domainDNS objects. The flags specify the types of attributes (also known as properties) represented by an attributeSchema object. For example, for the user object, there are three types of attributes: Domain-replicated and stored properties, Non-replicated and locally stored properties, and Non-stored and constructed properties.

typedef enum ADS_SYSTEMFLAG_ENUM {
    ADS_SYSTEMFLAG_DISALLOW_DELETE             = 0x80000000,
    ADS_SYSTEMFLAG_CONFIG_ALLOW_RENAME         = 0x40000000,
    ADS_SYSTEMFLAG_CONFIG_ALLOW_MOVE           = 0x20000000,
    ADS_SYSTEMFLAG_CONFIG_ALLOW_LIMITED_MOVE   = 0x10000000,
    ADS_SYSTEMFLAG_DOMAIN_DISALLOW_RENAME      = 0x08000000,
    ADS_SYSTEMFLAG_DOMAIN_DISALLOW_MOVE        = 0x04000000,
    ADS_SYSTEMFLAG_CR_NTDS_NC                  = 0x00000001,
    ADS_SYSTEMFLAG_CR_NTDS_DOMAIN              = 0x00000002,
    ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED         = 0x00000001,
    ADS_SYSTEMFLAG_ATTR_IS_CONTRUCTED          = 0x00000004
    }

Elements

ADS_SYSTEMFLAG_DISALLOW_DELETE
The attribute cannot be deleted.
ADS_SYSTEMFLAG_CONFIG_ALLOW_RENAME
The configuration property can be renamed.
ADS_SYSTEMFLAG_CONFIG_ALLOW_MOVE
The configuration property can be moved.
ADS_SYSTEMFLAG_CONFIG_ALLOW_LIMITED_MOVE
The configuration property can be moved with restrictions.
ADS_SYSTEMFLAG_DOMAIN_DISALLOW_RENAME
The domain property cannot be renamed.
ADS_SYSTEMFLAG_DOMAIN_DISALLOW_MOVE
The domain property cannot be moved.
ADS_SYSTEMFLAG_CR_NTDS_NC
Naming context is in NTDS.
ADS_SYSTEMFLAG_CR_NTDS_DOMAIN
Naming context is a domain.
ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED
The attribute is not to be replicated.
ADS_SYSTEMFLAG_ATTR_IS_CONTRUCTED
The attribute is a constructed property.

Example Code [C++]

The following code snippet shows how elements of the ADS_SYSTEMFLAG_ENUM enumeration, together with the IDirectorySearch interface, are used to search non-replicated properties.

#include <wchar.h>
#include <activeds.h>

HRESULT hr = E_FAIL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
IDirectorySearch *pSchemaNC = NULL;
IADs *pObject = NULL;
VARIANT var;

CoInitialize(NULL);     // initialize COM

//Get rootDSE and the schema container's distinguished name.
//Bind to current user's domain using current user's security context.
hr = ADsOpenObject(L"LDAP://rootDSE",
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication
                IID_IADs,
                (void**)&pObject);
if (SUCCEEDED(hr))
{
    hr = pObject->Get(L"schemaNamingContext",&var);
    if (SUCCEEDED(hr))
    {
    wcscpy(szPath,L"LDAP://");
    wcscat(szPath,var.bstrVal);
    hr = ADsOpenObject(szPath,
              NULL,
              NULL,
              ADS_SECURE_AUTHENTICATION,
              IID_IDirectorySearch,
              (void**)&pSchemaNC);
    if (SUCCEEDED(hr))
    {
    wprintf(L"Find non-replicated attributes\n");

    //Create search filter to find attributes with systemFlags that
    //match ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED
    LPOLESTR pszSearchFilter = new OLECHAR[MAX_PATH*2];
    wsprintf(pszSearchFilter, L"(&(objectCategory=attributeSchema)(systemFlags:1.2.840.113556.1.4.804:=%d))", ADS_SYSTEMFLAG_ATTR_NOT_REPLICATED);

    //Attributes are one-level deep in the Schema container
    //so only need to search one level.
    ADS_SEARCHPREF_INFO SearchPrefs;
    SearchPrefs.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
    SearchPrefs.vValue.dwType = ADSTYPE_INTEGER;
    SearchPrefs.vValue.Integer = ADS_SCOPE_ONELEVEL;
    DWORD dwNumPrefs = 1;

    // COL for iterations
    ADS_SEARCH_COLUMN    col;

    // Handle used for searching
    ADS_SEARCH_HANDLE hSearch;

    IADs    *pObj = NULL;
    IADs    * pIADs = NULL;

    // Set the search preference
   hr = pSchemaNC->SetSearchPreference( &SearchPrefs, dwNumPrefs);
   if (FAILED(hr)) return hr;

    CONST DWORD dwAttrNameSize = 1;
    LPOLESTR pszAttribute[dwAttrNameSize];
    pszAttribute[0] = L"cn";
    // Execute the search
    hr = pSchemaNC->ExecuteSearch(pszSearchFilter,
                                  pszAttribute,
                                  dwAttrNameSize,
                                  &hSearch );
    if ( SUCCEEDED(hr) ) {
        // Call IDirectorySearch::GetNextRow() to retrieve
        // the next row of data
        while( pSchemaNC->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS) {
            // loop through the array of passed column names,
            // print the data for each column
            for (DWORD x = 0; x < dwAttrNameSize; x++) {
                // Get the data for this column
                hr = pSchemaNC->GetColumn( hSearch,
                                           pszAttribute[x],
                                           &col );
                if ( SUCCEEDED(hr) ) {
                    // Print the data for the column and
                    // free the column
                    wprintf(L"%s: %s\r\n",
                            pszAttribute[x],
                            col.pADsValues->CaseIgnoreString);
                    pSchemaNC->FreeColumn( &col );
                }
                else
                    wprintf(L"<%s property is not a string>",
                             pszAttribute[x]);
            }
        }
        // Close the search handle to clean up
        pSchemaNC->CloseSearchHandle(hSearch);
    }

if (pObject)
    pObject->Release();
VariantClear(&var);

CoUninitialize();    // uninitialize COM.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SCOPEENUM

The ADS_SCOPEENUM enumeration specifies the scope of a directory search.

typedef enum {
    ADS_SCOPE_BASE        = 0,
    ADS_SCOPE_ONELEVEL    = 1,
    ADS_SCOPE_SUBTREE     = 2
    } ADS_SCOPEENUM;

Elements

ADS_SCOPE_BASE
Limits the search to the base object. The result contains at most one object.
ADS_SCOPE_ONELEVEL
Searches one level of the immediate children, excluding the base object.
ADS_SCOPE_SUBTREE
Searches the whole subtree, including all the children and the base object itself.

Example Code [C++]

Search scope is one of the search preferences clients can specify. The following code snippet illustrates how to accomplish this using the ADS_SEARCHPREF_INFO structure, together with the elements defined in the ADS_SEARCHPREF and this enumeration.

ADS_SEARCHPREF_INFO prefInfo;
prefInfo.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
prefInfo.vValue.dwType = ADSTYPE_INTEGER;
prefInfo.vValue.Integer = ADS_SCOPE_SUBTREE;

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SD_CONTROL_ENUM

The ADS_SD_CONTROL_ENUM enumeration specifies control flags for a security descriptor.

enum {
    ADS_SD_CONTROL_SE_OWNER_DEFAULTED       = 0x0001,
    ADS_SD_CONTROL_SE_GROUP_DEFAULTED       = 0x0002,
    ADS_SD_CONTROL_SE_DACL_PRESENT          = 0x0004,
    ADS_SD_CONTROL_SE_DACL_DEFAULTED        = 0x0008,
    ADS_SD_CONTROL_SE_SACL_PRESENT          = 0x0010,
    ADS_SD_CONTROL_SE_SACL_DEFAULTED        = 0x0020,
    ADS_SD_CONTROL_SE_DACL_AUTO_INHERIT_REQ = 0x0100,
    ADS_SD_CONTROL_SE_SACL_AUTO_INHERIT_REQ = 0x0200,
    ADS_SD_CONTROL_SE_DACL_AUTO_INHERITED   = 0x0400,
    ADS_SD_CONTROL_SE_SACL_AUTO_INHERITED   = 0x0800,
    ADS_SD_CONTROL_SE_DACL_PROTECTED        = 0x1000,
    ADS_SD_CONTROL_SE_SACL_PROTECTED        = 0x2000
    ADS_SD_CONTROL_SE_SELF_RELATIVE         = 0x8000,
    } ADS_SD_CONTROL_ENUM;

Elements

ADS_SD_CONTROL_SE_OWNER_DEFAULTED
A default mechanism provides the owner security identifier (SID) of the security descriptor rather than the original provider of the security descriptor.
ADS_SD_CONTROL_SE_GROUP_DEFAULTED
A default mechanism provides the group SID of the security descriptor rather than the original provider of the security descriptor.
ADS_SD_CONTROL_SE_DACL_PRESENT
The discretionary access-control list (DACL) is present in the security descriptor. If this flag is not set, or if this flag is set and the DACL is NULL, the security descriptor allows full access to everyone.
ADS_SD_CONTROL_SE_DACL_DEFAULTED
The security descriptor uses a default DACL built from the creator's access token.
ADS_SD_CONTROL_SE_SACL_PRESENT
The system access-control list (SACL) is present in the security descriptor.
ADS_SD_CONTROL_SE_SACL_DEFAULTED
The security descriptor uses a default SACL built from the creator's access token.
ADS_SD_CONTROL_SE_DACL_AUTO_INHERIT_REQ
THE DACL of the security descriptor must be inherited.
ADS_SD_CONTROL_SE_SACL_AUTO_INHERIT_REQ
The SACL of the security descriptor must be inherited.
ADS_SD_CONTROL_SE_DACL_AUTO_INHERITED
The DACL of the security descriptor supports automatic propagation of inheritable access-control entries (ACEs) to existing child objects.
ADS_SD_CONTROL_SE_SACL_AUTO_INHERITED
The SACL of the security descriptor supports automatic propagation of inheritable ACEs to existing child objects.
ADS_SD_CONTROL_SE_DACL_PROTECTED
The security descriptor will not allow inheritable ACEs to modify the DACL.
ADS_SD_CONTROL_SE_SACL_PROTECTED
The security descriptor will not allow inheritable ACEs to modify the SACL.
ADS_SD_CONTROL_SE_SELF_RELATIVE
The security descriptor is of self-relative format with all the security information in a continuous block of memory.

Remarks

For more information, see Access Control under Security in the Microsoft Platform SDK documentation.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SD_REVISION_ENUM

The ADS_SD_REVISION_ENUM enumeration specifies the revision number of the access-control entry (ACE), or the access-control list (ACL), for Active Directory.

enum {
    ADS_SD_REVISION_DS  = 4
    } ADS_SD_REVISION_ENUM;

Elements

ADS_SD_REVISION_DS
The revision number of the ACE, or the ACL, for Active Directory.

Remarks

The ADS_SD_REVISION_DS flag signifies that the related ACL contains object-specific ACEs.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SEARCHPREF

The ADS_SEARCHPREF enumeration specifies preferences for the directory search.

typedef enum {
    ADS_SEARCHPREF_ASYNCHRONOUS      = 0,
    ADS_SEARCHPREF_DEREF_ALIASES,
    ADS_SEARCHPREF_SIZE_LIMIT,
    ADS_SEARCHPREF_TIME_LIMIT,
    ADS_SEARCHPREF_ATTRIBTYPES_ONLY,
    ADS_SEARCHPREF_SEARCH_SCOPE,
    ADS_SEARCHPREF_TIMEOUT,
    ADS_SEARCHPREF_PAGESIZE,
    ADS_SEARCHPREF_PAGED_TIME_LIMIT,
    ADS_SEARCHPREF_CHASE_REFERRALS,
    ADS_SEARCHPREF_SORT_ON,
    ADS_SEARCHPREF_CACHE_RESULTS
    } ADS_SEARCHPREF_ENUM;
typedef ADS_SEARCHPREF_ENUM ADS_SEARCHPREF;

Elements

ADS_SEARCHPREF_ASYNCHRONOUS
Specifies that searches should be carried out in an asynchronous way.
ADS_SEARCHPREF_DEREF_ALIASES
Specifies that aliases of found objects are to be resolved. Use the ADS_DEREFENUM enumeration to specify how this should be done.
ADS_SEARCHPREF_SIZE_LIMIT
Specifies the size limit that the server should observe in a search. For Active Directory, the size limit specifies the maximum number of returned objects. The server stops searching once the size limit is reached and returns the results accumulated up to that point.
ADS_SEARCHPREF_TIME_LIMIT
Specifies the time limit (in seconds) for the search that the server should observe in a search. When the time limit is reached, the server stops searching and returns whatever results it has accumulated up to that point.
ADS_SEARCHPREF_ATTRIBTYPES_ONLY
Indicates that the search should obtain only the name of attributes to which values have been assigned.
ADS_SEARCHPREF_SEARCH_SCOPE
Specifies the search scope that should be observed by the server. For the appropriate settings, see the ADS_SCOPEENUM enumeration.
ADS_SEARCHPREF_TIMEOUT
Specifies the time limit (in seconds) that a client is willing to wait for the server to return the result. This option is set in an ADS_SEARCHPREF_INFO structure.
ADS_SEARCHPREF_PAGESIZE
Specifies the page size in a paged search. For each request by the client, the server returns at most the number of objects as set by the page size.
ADS_SEARCHPREF_PAGED_TIME_LIMIT
Specifies the time limit (in seconds) that the server should observe to search a page of results (as opposed to the time limit for the entire search). When the limit is reached, the server stops searching and returns the result obtained up to that point, along with a cookie containing the information about where to resume searching.
ADS_SEARCHPREF_CHASE_REFERRALS
Specifies that referrals may be chased. If the root search is not specified in the naming context of the server or when the search results cross a naming context (for example, when you have child domains and search in the parent domain), the server sends a referral message to the client which the client can choose to ignore or chase. For more information on referrals chasing, see ADS_CHASE_REFERRALS_ENUM.
ADS_SEARCHPREF_SORT_ON
Specifies that the server sorts the result set. Use the ADS_SORTKEY structure to specify the sort keys.
ADS_SEARCHPREF_CACHE_RESULTS
Specifies if the result should be cached on the client side. By default, ADSI caches the result set. Turning off this option may be more desirable for large result sets.

Remarks

To set up a search preference, you assign appropriate values to the fields of an ADS_SEARCHPREF_INFO structure that will be passed to the server. To set up multiple preferences, you need to use an array of ADS_SEARCHPREF_INFO structures. The member values of this enumeration are assigned to the dwSearchPref field of the structure.

Example Code [C++]

The following code snippet illustrates how to set up search preferences using the ADS_SEARCHPREF enumeration.

HRESULT SetSearchPreferences2(
               DWORD dwScope,//-1 means use default: subtree
               DWORD dwOverallTimeOut,//<=0 means use default: no time out set.
               DWORD dwOverallSizeLimit,//<=0 means use default: no size limit set.
               DWORD dwOverallTimeLimit,//<=0 means use default: no time limit set.
               BOOL bCacheResult,//True means use default.
               BOOL bIsAsynchronous,//False means use default
               DWORD dwPageSize,//<=0 means use default
               DWORD dwPageTimeLimit,//<=0 means use default
               DWORD dwChaseReferral,//<=0 means use default
               LPOLESTR szSortKey,//NULL means don't sort.
               BOOL bIsDescending,
               BOOL bReturnAttributeNamesOnly,//False means use default.
               ADS_SEARCHPREF_INFO **ppSearchPref, //Return an array of search preferences.
              DWORD *pdwSearchPrefCount
)
{
   HRESULT hr = S_OK;
   DWORD dwCountPref = 0L;

   //Determine size of preferences array.
   DWORD dwTotal = 11L;

   if(dwScope==-1)
       dwTotal--;
   if(dwOverallTimeOut<=0)
       dwTotal--;
   if(dwOverallSizeLimit<=0)
       dwTotal--;
   if(dwOverallTimeLimit<=0)
       dwTotal--;
   if(bCacheResult)
       dwTotal--;
   if(!bIsAsynchronous)
       dwTotal--;
   if(dwPageSize<=0)
       dwTotal--;
   if(dwPageTimeLimit<=0)
       dwTotal--;
   if(dwChaseReferral<=0)
       dwTotal--;
   if(!bReturnAttributeNamesOnly)
       dwTotal--;
   if (!szSortKey)
       dwTotal--;

   ADS_SEARCHPREF_INFO *prefInfo = new ADS_SEARCHPREF_INFO[ dwTotal ];
   ADS_SORTKEY SortKey;

    //////////////////
    // Search Scope
    //////////////////
    if(dwScope>=0)
    {
        prefInfo[dwCountPref].dwSearchPref =
                         ADS_SEARCHPREF_SEARCH_SCOPE;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
        prefInfo[dwCountPref].vValue.Integer = dwScope;
        dwCountPref++;
    }

    //////////////////
    // Time Out
    //////////////////
    if(dwOverallTimeOut>0)
    {
       prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_TIMEOUT;
       prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
       prefInfo[dwCountPref].vValue.Integer = dwOverallTimeOut;
       dwCountPref++;
    }

    ///////////////
    // Size Limit
    ///////////////
    if(dwOverallSizeLimit>0)
    {
       prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_SIZE_LIMIT;
       prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
       prefInfo[dwCountPref].vValue.Integer = dwOverallSizeLimit;
       dwCountPref++;
    }

    ///////////////
    // Time Limit
    ///////////////
    if(dwOverallTimeLimit>0)
    {
       prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_TIME_LIMIT;
       prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
       prefInfo[dwCountPref].vValue.Integer = dwOverallTimeLimit;
       dwCountPref++;
    }

    /////////////////
    // Cache Result
    /////////////////

    if (!bCacheResult)
    {
        prefInfo[dwCountPref].dwSearchPref =
                              ADS_SEARCHPREF_CACHE_RESULTS;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
        prefInfo[dwCountPref].vValue.Boolean = bCacheResult;
        dwCountPref++;
    }

    //////////////
    // Page Size
    //////////////
    if(dwPageSize>0)
    {
        prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;;
        prefInfo[dwCountPref].vValue.Integer = dwPageSize;
        dwCountPref++;
    }

    //////////////
    // Page Time Limit
    //////////////
    if(dwPageTimeLimit>0)
    {
        prefInfo[dwCountPref].dwSearchPref =
                                      ADS_SEARCHPREF_PAGED_TIME_LIMIT;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;;
        prefInfo[dwCountPref].vValue.Integer = dwPageTimeLimit;
        dwCountPref++;
    }

    ///////////////////
    // Chase Referrals
    ///////////////////
    if(dwChaseReferral>0)
    {
        prefInfo[dwCountPref].dwSearchPref =
                                      ADS_SEARCHPREF_CHASE_REFERRALS;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
        prefInfo[dwCountPref].vValue.Integer = dwChaseReferral;
        dwCountPref++;
    }

    /////////////
    // Sort
    /////////////
    if (szSortKey)
    {
        prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_SORT_ON;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_PROV_SPECIFIC;
        SortKey.pszAttrType = (LPWSTR)LocalAlloc(
                        LPTR,
                        wcslen(szSortKey)*sizeof(WCHAR) +sizeof(WCHAR)
                        );
        wcscpy(SortKey.pszAttrType,szSortKey);
        SortKey.pszReserved = NULL;
        SortKey.fReverseorder = 0;
        prefInfo[dwCountPref].vValue.ProviderSpecific.dwLength =
                                                 sizeof(ADS_SORTKEY);
        prefInfo[dwCountPref].vValue.ProviderSpecific.lpValue =
                                                 (LPBYTE) &SortKey;
        dwCountPref++;
    }

    /////////////////
    // Asynchronous
    /////////////////
    if(bIsAsynchronous)
    {
        prefInfo[dwCountPref].dwSearchPref =
                                     ADS_SEARCHPREF_ASYNCHRONOUS;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
        prefInfo[dwCountPref].vValue.Integer = bIsAsynchronous;
        dwCountPref++;
    }

    ////////////////////////
    // Attribute Type Only
    ////////////////////////
    if(bReturnAttributeNamesOnly)
    {
        prefInfo[dwCountPref].dwSearchPref =
                                  ADS_SEARCHPREF_ATTRIBTYPES_ONLY;
        prefInfo[dwCountPref].vValue.dwType = ADSTYPE_BOOLEAN;
        prefInfo[dwCountPref].vValue.Integer =
                                  bReturnAttributeNamesOnly;
        dwCountPref++;
    }

    if (SUCCEEDED(hr))
    {
        *pdwSearchPrefCount = dwCountPref;
        *ppSearchPref  = prefInfo;
    }
    else
    {
        *pdwSearchPrefCount = 0L;
        *ppSearchPref  = NULL;
    }

    return hr;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SECURITY_INFO_ENUM

The ADS_SECURITY_INFO_ENUM enumeration specifies the available options for examining security information of an object.

enum {
    ADS_SECURITY_INFO_OWNER = 0x1,
    ADS_SECURITY_INFO_GROUP = 0x2,
    ADS_SECURITY_INFO_DACL  = 0x4,
    ADS_SECURITY_INFO_SACL  = 0x8
    } ADS_SECURITY_INFO_ENUM;

Elements

ADS_SECURITY_INFO_OWNER
Reads or sets the owner information.
ADS_SECURITY_INFO_GROUP
Reads or sets the group information.
ADS_SECURITY_INFO_DACL
Reads or sets the discretionary access-control list (DACL).
ADS_SECURITY_INFO_SACL
Reads or sets the system access-control list (SACL).

Remarks

The options defined in this enumeration are bit-masks. More than one option can be set using appropriate bit-wise operations.

To read the security information for an object, use the methods of the IADsObjectOptions interface, supplying the security information options listed in this enumeration. For example, assuming obj is an object implementing the IADsObjectOptions interface, the following example will allow users to read the security information of the owner, group, or DACL of an object.

obj.SetOption ADS_OPTION_SECURITY_MASK, ADS_SECURITY_INFO_OWNER _
                                      Or ADS_SECURITY_INFO_GROUP _
                                      Or ADS_SECURITY_INFO_DACL

In fact, this is the default setting when an object is created. To allow users to read the system access-control list (SACL), you must explicitly set the SACL option by calling the IADsObjectOptions::SetOption method, as shown in the following:

obj.SetOption ADS_OPTION_SECURITY_MASK, ADS_SECURITY_INFO_OWNER _
                                      Or ADS_SECURITY_INFO_GROUP _
                                      Or ADS_SECURITY_INFO_DACL _
                                      Or ADS_SECURITY_INFO_SACL

You cannot use the following syntax, even if you are only interested in the SACL:

obj.SetOption ADS_OPTION_SECURITY_MASK, ADS_SECURITY_INFO_SACL

Once the SACL option is set, you can proceed to read the SACL of the object.

Dim sd as IADsSecurityDescriptor
Dim sacl as IADsAccessControlList

obj.GetInfo
set sd = obj.GetEx("ntSecurityDescriptor")
set sacl = sd.SystemAcl
Debug.Print sacl.AceCount

In general, to find out if you can read the SACL, you may want to use the IADsObjectOptions::GetOption method to make sure that the option has been set.

Dim opt, canReadSACL As Var
canReadSACL =   ADS_SECURITY_INFO_OWNER _
             Or ADS_SECURITY_INFO_GROUP _
             Or ADS_SECURITY_INFO_DACL _
             Or ADS_SECURITY_INFO_SACL
opt = obj.GetOption(ADS_OPTION_SECURITY_MASK)
if opt = canReadSACL then
    ' read SACL
end if

Presently, such options are available for Active Directory only.

Example Code [Visual Basic]

The following Visual Basic code snippet displays the number of access control entries in a system Acl.

Dim x As IADs
Dim dso As IADsOpenDSObject
Dim adsPath As String
Dim sd As IADsSecurityDescriptor
Dim sacl As IADsAccessControlList
Dim objOps As IADsObjectOptions
Dim opt As Variant
Dim canReadSacl, canReadDacl, canReadOwner, canReadGroup As Variant

Set dso = GetObject("LDAP:")
adsPath = "LDAP://ArcSrv1/dc=Sales,dc=ArcadiaBay,dc=com"
Set x = dso.OpenDSObject(adsPath, "Administrator", "", 1)
Set objOps = x

canReadOwner = ADS_SECURITY_INFO_OWNER

canReadGroup = ADS_SECURITY_INFO_OWNER _
                Or ADS_SECURITY_INFO_GROUP

canReadDacl = ADS_SECURITY_INFO_OWNER _
                Or ADS_SECURITY_INFO_GROUP _
                Or ADS_SECURITY_INFO_DACL

canReadSacl = ADS_SECURITY_INFO_OWNER _
                Or ADS_SECURITY_INFO_GROUP _
                Or ADS_SECURITY_INFO_DACL _
                Or ADS_SECURITY_INFO_SACL

opt = objOps.GetOption(ADS_OPTION_SECURITY_MASK)
If opt <> canReadSacl Then
    objOps.SetOption ADS_OPTION_SECURITY_MASK, canReadSacl
End If
Set sd = x.Get("ntSecurityDescriptor")
Set sacl = sd.SystemAcl
Debug.Print "sacl(aceCount)= " & sacl.AceCount

Example Code [C++]

The following C++ code snippet displays the number of access control entries in a system Acl. For brevity, error checking is omitted.

void TestObjectOptions()
{
   IADsObjectOptions *pObjOps;
   IADs *pObj;
   IADsSecurityDescriptor *pSd;
   IADsAccessControlList *pSacl;
   IDispatch *pDisp;

   long canReadOwner = ADS_SECURITY_INFO_OWNER;
   long canReadGroup = canReadOwner | ADS_SECURITY_INFO_GROUP;
   long canReadDACL  = canReadGroup | ADS_SECURITY_INFO_DACL;
   long canReadSACL  = canReadDACL  | ADS_SECURITY_INFO_SACL;
   HRESULT hr = S_OK;

   LPWSTR adsPath = L"LDAP://arcSrv1/dc=Sales,dc=ArcadiaBay,dc=com";
   LPWSTR usrName = L"Administrator";
   LPWSTR usrPass = L"";

   long readOwner, readGroup, readDacl, readSacl;

    readOwner = ADS_SECURITY_INFO_OWNER;

    readGroup = ADS_SECURITY_INFO_OWNER
              | ADS_SECURITY_INFO_GROUP;

    readDacl  = ADS_SECURITY_INFO_OWNER
              | ADS_SECURITY_INFO_GROUP
              | ADS_SECURITY_INFO_DACL;

    readSacl  = ADS_SECURITY_INFO_OWNER
              | ADS_SECURITY_INFO_GROUP
              | ADS_SECURITY_INFO_DACL
              | ADS_SECURITY_INFO_SACL;

   hr = ADsOpenObject(adsPath,
                      usrName,
                      usrPass,
                      ADS_SECURE_AUTHENTICATION,
                      IID_IADs,(void**)&pObj);
   hr = pObj->QueryInterface(IID_IADsObjectOptions,(void**)&pObjOps);

   long opt;
   VARIANT var;
   VariantInit(&var);
   hr = pObjOps->GetOption(ADS_OPTION_SECURITY_MASK,&var);
   opt = V_I4(&var);
   VariantClear(&var);
   if(opt != canReadSACL) {
       V_I4(&var)=canReadSACL;
       V_VT(&var)=VT_I4;
       hr = pObjOps->SetOption(ADS_OPTION_SECURITY_MASK, var);
   }

   hr = pObj->Get(L"ntSecurityDescriptor",&var);
   hr = V_DISPATCH(&var)->QueryInterface(IID_IADsSecurityDescriptor,
                                         (void**)&pSd);
   hr = pSd->get_SystemAcl(&pDisp);
   hr = pDisp->QueryInterface(IID_IADsAccessControlList,
                              (void**)&pSacl);
   hr = pSacl->get_AceCount(&opt);
   printf("Number of ACE's in the SACL is %d\n",opt);

   pSacl->Release();
   pDisp->Release();
   pSd->Release();
   VariantClear(&var);
   pObjOps->Release();
   pObj->Release();
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_SETTYPE

The ADS_SETTYPE enumeration specifies the available pathname format used by the IADsPathname::Set method.

typedef enum ADS_SETTYPE {
    ADS_SETTYPE_FULL      = 1,
    ADS_SETTYPE_PROVIDER  = 2,
    ADS_SETTYPE_NAMESPACE = 2,
    ADS_SETTYPE_SERVER    = 3,
    ADS_SETTYPE_DN        = 4
    } ADS_SETTYPE, *PADS_SETTYPE;

Elements

ADS_SETTYPE_FULL
Sets the full path, for example, "LDAP://servername/o=internet/…/cn=bar".
ADS_SETTYPE_PROVIDER
Updates the provider only, for example, "LDAP".
ADS_SETTYPE_NAMESPACE
The same as ADS_SETTYPE_PROVIDER.
ADS_SETTYPE_SERVER
Updates the server name only, for example, "servername".
ADS_SETTYPE_DN
Updates the distinguished name only, for example, "o=internet/…/cn=bar".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_STATUS

The ADS_STATUS enumeration specifies whether a search preference has been set and, if not, why not.

typedef enum {
    ADS_STATUS_S_OK  = 0,
    ADS_STATUS_INVALID_SEARCHPREF,
    ADS_STATUS_INVALID_SEARCHPREFVALUE
    } ADS_STATUSENUM;
typedef ADS_STATUSENUM ADS_STATUS, *PADS_STATUS;

Elements

ADS_STATUS_S_OK
The search preference was set successfully.
ADS_STATUS_INVALID_SEARCHPREF
The search preference specified in the dwSearchPref field of the ADS_SEARCHPREF_INFO structure is invalid. Search preferences must be taken from the ADS_SEARCHPREF enumeration.
ADS_STATUS_INVALID_SEARCHPREFVALUE
The value specified in the vValue member of the ADS_SEARCHPREF_INFO structure is invalid for the corresponding search preference.

Remarks

The IDirectorySearch::SetSearchPreference method writes a member value of the ADS_STATUS enumeration to the dwStatus member of an ADS_SEARCHPREF_INFO structure to indicate whether the corresponding search preference was set. Callers can use this status value to decide whether to execute a search.

Example Code [C++]

The following C/C++ code snippet illustrates how to use ADS_STATUS with IDirectorySearch::SetSearchPreference in a directory search application.

ADS_SEARCHPREF_INFO pSearchPref[10];
LPWSTR pszSearchBase, pszSearchFilter, pszAttrNames[10];
DWORD dwNumberAttributes = -1;
DWORD dwCurrPref = 0;
LPWSTR pszUserName=NULL, pszPassword=NULL;
DWORD dwAuthFlags=0;
HRESULT hr=S_OK;
IDirectorySearch *pDSSearch=NULL;
ADS_SEARCH_HANDLE hSearchHandle=NULL;

hr = ADsOpenObject(
    pszSearchBase,
    pszUserName,
    pszPassword,
    dwAuthFlags,
    IID_IDirectorySearch,
    (void **)&pDSSearch
    );

if (dwCurrPref) {
    hr = pDSSearch->SetSearchPreference(
        pSearchPref,
        dwCurrPref
        );
    if (hr != S_OK) {
        for (i=0; i<dwCurrPref; i++) {
            if (pSearchPref[i].dwStatus != ADS_STATUS_S_OK) {
                printf(
                "Error in setting the preference %s: status = %d\n",
                 prefNameLookup[pSearchPref[i].dwSearchPref],
                 pSearchPref[i].dwStatus
                 );
                 cErr++;
            }
        }
    }
}

hr = pDSSearch->ExecuteSearch(
    pszSearchFilter,
    pszAttrNames,
    dwNumberAttributes,
    &hSearchHandle
    );

hr = pDSSearch->GetNextRow(
    hSearchHandle
    );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADS_USER_FLAG

The ADS_USER_FLAG enumeration defines the flags used for manipulating various user properties in the directory.

typedef enum ADS_USER_FLAG {
    ADS_UF_SCRIPT                           =  0X0001,
    ADS_UF_ACCOUNTDISABLE                   =  0X0002,
    ADS_UF_HOMEDIR_REQUIRED                 =  0X0003,
    ADS_UF_LOCKOUT                          =  0X0010,
    ADS_UF_PASSWD_NOTREQD                   =  0X0020,
    ADS_UF_PASSWD_CANT_CHANGE               =  0X0040,
    ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED  =  0X0080,
    ADS_UF_TEMP_DUPLICATE_ACCOUNT           =  0X0100,
    ADS_UF_NORMAL_ACCOUNT                   =  0X0200,
    ADS_UF_INTERDOMAIN_TRUST_ACCOUNT        =  0X0800,
    ADS_UF_WORKSTATION_TRUST_ACCOUNT        =  0X1000,
    ADS_UF_SERVER_TRUST_ACCOUNT             =  0X2000,
    ADS_UF_DONTEXPIREPASSWD                 =  0X10000,
    ADS_UF_MNS_LOGON_ACCOUNT                =  0X20000,
    ADS_UF_SMARTCARD_REQUIRED               =  0X40000,
    ADS_UF_TRUSTED_FOR_DELEGATION           =  0X80000,
    ADS_UF_NOT_DELEGATED                    =  0X100000
    }

Elements

ADS_UF_SCRIPT
The logon script will be executed.
ADS_UF_ACCOUNTDISABLE
The user's account is disabled.
ADS_UF_HOMEDIR_REQUIRED
The home directory is required.
ADS_UF_LOCKOUT
The account is currently locked out.
ADS_UF_PASSWD_NOTREQD
No password is required.
ADS_UF_PASSWD_CANT_CHANGE
The user cannot change the password.
ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
The user can send an encrypted password.
ADS_UF_TEMP_DUPLICATE_ACCOUNT
This is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. Sometimes it is referred to as a local user account.
ADS_UF_NORMAL_ACCOUNT
This is a default account type that represents a typical user.
ADS_UF_INTERDOMAIN_TRUST_ACCOUNT
This is a permit to trust account for a system domain that trusts other domains.
ADS_UF_WORKSTATION_TRUST_ACCOUNT
This is a computer account for a Windows NT® Workstation/Windows® 2000 Professional or Windows NT® Server/Windows® 2000 Server that is a member of this domain.
ADS_UF_SERVER_TRUST_ACCOUNT
This is a computer account for a system backup domain controller that is a member of this domain.
ADS_UF_DONTEXPIREPASSWD
Represents the password, which should never expire on the account.
ADS_UF_MNS_LOGON_ACCOUNT
This is an MNS logon account.
ADS_UF_SMARTCARD_REQUIRED
When set, this flag will force the user to log on using smart card.
ADS_UF_TRUSTED_FOR_DELEGATION
When set, the service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service. To enable a service for Kerberos delegation, you must set this flag on the userAccountControl property of the service account.
ADS_UF_NOT_DELEGATED
When set, the security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation.

Remarks

For more information, see the "Managing Users" topic in the Active Directory Programmer's Guide.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use the user flag defined above to prevent a user from changing his or her password.

Set usr = GetObject("WinNT://INDEPENDENCE/jsmith,user")
usr.Put "userFlags", usr.Get("UserFlags") Or ADS_UF_PASSWORD_CANT_CHANGE
usr.SetInfo

The following code snippet clears the flag to allow a user to change the password.

usr.Put "userFlags", usr.Get("UserFlags") Xor ADS_UF_PASSWORD_CANT_CHANGE
usr.SetInfo

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

NetUserGetInfo


ADSI_DIALECT_ENUM

The ADSI_DIALECT_ENUM enumeration specifies query dialects used in the OLE DB provider for ADSI.

typedef enum {
    ADSI_DIALECT_LDAP   = 0,
    ADSI_DIALECT_SQL    = 0x1
    } ADSI_DIALECT_ENUM

Elements

ADSI_DIALECT_LDAP
ADSI queries are based on the LDAP dialect.
ADSI_DIALECT_SQL
ADSI queries are based on the SQL dialect.

Remarks

An Active Data Object (ADO) client can use one of the two ADSI query dialects to query a directory. For more information on the ADSI query dialects, see "Searching" in the Active Directory Service Interfaces Programmer's Guide.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADSTYPE

The ADSTYPE enumeration specifies data types to use to interpret an ADSI extended syntax string.

typedef enum {
    ADSTYPE_INVALID = 0,
    ADSTYPE_DN_STRING    ,
    ADSTYPE_CASE_EXACT_STRING,
    ADSTYPE_CASE_IGNORE_STRING,
    ADSTYPE_PRINTABLE_STRING,
    ADSTYPE_NUMERIC_STRING,
    ADSTYPE_BOOLEAN,
    ADSTYPE_INTEGER,
    ADSTYPE_OCTET_STRING,
    ADSTYPE_UTC_TIME,
    ADSTYPE_LARGE_INTEGER,
    ADSTYPE_PROV_SPECIFIC,
    ADSTYPE_OBJECT_CLASS,
    ADSTYPE_CASEIGNORE_LIST,
    ADSTYPE_OCTET_LIST,
    ADSTYPE_PATH,
    ADSTYPE_POSTALADDRESS,
    ADSTYPE_TIMESTAMP,
    ADSTYPE_BACKLINK,
    ADSTYPE_TYPEDNAME,
    ADSTYPE_HOLD,
    ADSTYPE_NETADDRESS,
    ADSTYPE_REPLICAPOINTER,
    ADSTYPE_FAXNUMBER,
    ADSTYPE_EMAIL,
    ADSTYPE_NT_SECURITY_DESCRIPTOR,
    ADSTYPE_UNKNOWN
    } ADSTYPEENUM;

typedef ADSTYPEENUM ADSTYPE;

Elements

ADSTYPE_INVALID
The data is of an invalid type.
ADSTYPE_DN_STRING
The string is of Distinguished Name (path) of a directory service object.
ADSTYPE_CASE_EXACT_STRING
The string is of the case-sensitive type.
ADSTYPE_CASE_IGNORE_STRING
The string is of the case-insensitive type.
ADSTYPE_PRINTABLE_STRING
The string is displayable on screen or in print.
ADSTYPE_NUMERIC_STRING
The string is of a numeral to be interpreted as text.
ADSTYPE_BOOLEAN
The data is of a Boolean value.
ADSTYPE_INTEGER
The data is of an integer value.
ADSTYPE_OCTET_STRING
The string is of a byte array.
ADSTYPE_UTC_TIME
The data is of the universal time as expressed in Universal Time Coordinates (UTC).
ADSTYPE_LARGE_INTEGER
The data is of a long integer value.
ADSTYPE_PROV_SPECIFIC
The string is of a provider-specific string.
ADSTYPE_OBJECT_CLASS
The string is of an object class.
ADSTYPE_CASEIGNORE_LIST
The data is of a list of case insensitive strings.
ADSTYPE_OCTET_LIST
The data is of a list of octet strings.
ADSTYPE_PATH
The string is of a directory path.
ADSTYPE_POSTALADDRESS
The string is of the postal address type.
ADSTYPE_TIMESTAMP
The data is of a time stamp in seconds.
ADSTYPE_BACKLINK
The string is of a back link.
ADSTYPE_TYPEDNAME
The string is of a typed name.
ADSTYPE_HOLD
The data is of the Hold data structure.
ADSTYPE_NETADDRESS
The string is of a net address.
ADSTYPE_REPLICAPOINTER
The data is of a replica pointer.
ADSTYPE_FAXNUMBER
The string is of a fax number.
ADSTYPE_EMAIL
The data is of an e-mail message.
ADSTYPE_NT_SECURITY_DESCRIPTOR
The data is of Windows NT®/Windows® 2000 security descriptor as represented by a byte array.
ADSTYPE_UNKNOWN
The data is of an undefined type.

Example Code [C++]

The following C code snippet illustrates certain usage of the member values of the ADSTYPE enumeration.

ADS_SEARCHPREF_INFO prefInfo;
BOOL SearchScopeDefined;

prefInfo.dwSearchPref = ADS_SEARCHPREF_SEARCH_SCOPE;
if (SearchScopeDefined) {
    prefInfo.vValue.dwType = ADSTYPE_INTEGER;
    prefInfo.vValue.Integer = ADS_SCOPE_SUBTREE;
}
else
    prefInfo.vValue.dwType = ADSTYPE_BOOLEAN;
    prefInfo.vValue.Boolean = FALSE;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


ADSI Functions

Active Directory™ Service Interfaces expose the following helper functions to clients that do not use Automation.

Function | Description
ADsGetObject | Binds to an ADSI object using the current credentials.
ADsOpenObject | Binds to an ADSI object using specified credentials
ADsGetLastError | Retrieves the last error code value of the calling thread.
ADsSetLastError | Sets the error code value of the calling thread.
ADsBuildVarArrayInt | Builds a variant array from an array of DWORDs.
ADsBuildVarArrayStr | Builds a variant array from an array of Unicode strings.
ADsBuildEnumerator | Creates an enumerator object for the specified ADSI container object.
ADsEnumerateNext | Populates a variant array with elements retrieved from the specified enumerator object.
ADsFreeEnumerator | Frees an enumerator object previously created by ADsBuildEnumerator.
AllocADsMem | Allocates a block of memory.
FreeADsMem | Frees the memory allocated by AllocADsMem.
ReallocADsMem | Assigns the existing memory content to a newly created memory location.
AllocADsStr | Allocates memory for a given string.
FreeADsStr | Frees the memory allocated for the given string.
ReallocADsStr | Replaces an existing string with a new one.
ADsEncodeBinaryData | Converts a blob of binary data to the format suitable for a search filter.


ADsBuildEnumerator

The ADsBuildEnumerator function builds an enumerator object for the specified ADSI container object.

HRESULT ADsBuildEnumerator(
  IADsContainer *pADsContainer,
  IEnumVARIANT **ppEnumVariant
);

Parameters

pADsContainer
[in] Pointer to the IADsContainer interface on the object to be enumerated.
ppEnumVariant
[out] Indirect pointer to the IEnumVARIANT interface on the enumerator object created for the specified container object.

Return Values

This method supports the standard HRESULT return values, including S_OK for a successful operation. For other return values, see ADSI Error Codes.

Remarks

The ADsBuildEnumerator helper function calls the IADsContainer::get__NewEnum method to create an enumerator object and then calls the QueryInterface method of IUnknown to get a pointer to the IEnumVARIANT interface on the enumerator object. The general procedure to enumerate the available objects in a container is as follows:

  1. Create an enumerator on that container and retrieve the IEnumVARIANT interface pointer.
  2. Call the ADsEnumerateNext function on the IEnumVARIANT interface.
  3. Upon finishing the enumeration, call ADsFreeEnumerator to release the enumerator.

The following code snippet illustrates how this and other related API functions can be used to delete all the objects in a container.

Void DeleteObject(IUnknown* pIUnknown)
{
IUnknown*       pIChildUnk   = NULL;
IADsContainer*  pIContainer  = NULL;
IEnumVARIANT*   pIEnumVar    = NULL;
ULONG           ulFetch      = 0;
VARIANT         var;
HRESULT         hr;

if(pIUnknown =NULL) return E_FAIL;

// QI container interface.
hr=pIUnknown->QueryInterface(IID_IADsContainer,(void**)&pIContainer);

if (SUCCEEDED(hr)) {
    //Create an enumerator object in the container.
    hr=ADsBuildEnumerator(pIContainer, &pIEnumVar);
    while(SUCCEEDED(hr)) {
        //Get the next contained object.
        ulFetch = 0L;
        hr = ADsEnumerateNext(pIEnumvar, 1, &var, &ulFetch);
        if(!ulFetch) break;
        //Check if the retrieved object is another container.
        V_DISPATCH(%var)->QueryInterface(IID_IUnknown,
                                        (void**)&pIChildUnk);
        //Release the contained object.
        VariantClear(&var);

        //Call the function recursively on child containers.
        if(pIChildUnk != NULL) {
            DeleteObject(pIContainer, pIChildUnk);
            pIChildUnk->Release();
        }
        pIChildUnk = NULL;
    }
    //Release the enumerator.
    if (pIEnumVar != NULL) {
        ADsFreeEnumerator(pIEnumVar);
    }
    //Release the container.
    pIContainer->Release();
}

If the server supports paged search and the client has specified the page limit that exceeds the maximum search results allowed on the server, the ADsBuildEnumerator function forwards errors and results from the server to the user in the following ways:

  1. If the server returns an error with no results, then the function returns only the error.
  2. If the server returns partial results with or without an error, the function returns the partial results from the server to the user.
  3. If the server returns all results, for example, the maximum number of search results on each page and all results through multiple pages, with or without an error, the function returns all the results from the server to the user.

Here is another code snippet that uses the ADsBuildEnumerator function to enumerate Group Membership.

int main(int argc, char* argv[])
{
    IADs            *pRoot=NULL;
    IADsContainer   *pCont=NULL;
    IEnumVARIANT    *pEnum=NULL,*pMbrEnum=NULL;
    IDispatch       *pDisp,*pDispMem;
    IADs            *pADs,*pADsMem;
    IADsGroup       *pGrp;
    IADsMembers     *pMbr;

    VARIANT varDSRoot;
    VARIANT varEnum,varEnumMem;
    ULONG   ulFetch=0;
    BSTR    bstrPath;

    TCHAR   adspath[MAX_PATH];

    HRESULT hr;

    hr = CoInitialize(NULL);

    //Get the name of the root container for this domain.
    //Read the Root DSE from the default DS, which will be the DS for
    //the local domain.  This will get us the name of the schema container,
    //which is stored in the "defaultNamingContext" operational attribute.

    hr = ADsGetObject(TEXT("LDAP://RootDSE"),
                      IID_IADs,
                      (void**)&pRoot);

    hr = pRoot->Get(TEXT("defaultNamingContext"),&varDSRoot);
    _tprintf(TEXT("\nDS Root :%s\n"),varDSRoot.bstrVal);
    pRoot->Release();

    //Now bind to the builtin container.

    _tcscpy(adspath,TEXT("LDAP://"));
    _tcscat(adspath,TEXT("CN=Builtin,"));
    _tcscat(adspath,varDSRoot.bstrVal);

    hr = ADsGetObject(adspath,IID_IADsContainer,(void**)&pCont);

    //Enumerate the contents of the builtin container

    hr = ADsBuildEnumerator(pCont,&pEnum);

    while (hr == S_OK) {
        //
        //Fetch one element and place it in varEnum.  ulFetch is the
        //number of elements actually returned.  If there are no objects
        //to enumerate ulFetch will be zero and the call will succeed.
        //When an object is fetched, varEnum  will contain the dispinterface
        //of the child object
        //
        hr = ADsEnumerateNext(pEnum,1,&varEnum,&ulFetch);
        if (ulFetch) {
            //
            //Use the dispinterface to QI for IADs
            //
            pDisp = V_DISPATCH(&varEnum);
            hr = pDisp->QueryInterface( IID_IADs, (void**)&pADs);
            hr = pADs->get_ADsPath(&bstrPath);
            if SUCCEEDED(hr) {
                _tprintf(TEXT(" Builtin:%s\n"),bstrPath);

                //Enumerate the members of this group. Bind to the
                //group requesting IADsGroup
                //
                hr = ADsGetObject(bstrPath,IID_IADsGroup,(void**)&pGrp);
                //Retrieve the Members property - this is a container
                //that holds IADs objects for the members
                //
                hr = pGrp->Members(&pMbr);
                //Create another enumerator and enumerate the members of
                //the group.
                //
                hr = ADsBuildEnumerator((IADsContainer*)pMbr,&pMbrEnum);
                while (hr == S_OK) {
                    hr = ADsEnumerateNext(pMbrEnum,1,&varEnumMem,&ulFetch);
                    if (ulFetch) {
                        //
                        //For each member, read the ADsPath attribute
                        //and display it.
                        //
                        pDispMem = V_DISPATCH(&varEnumMem);
                        hr = pDispMem->QueryInterface( IID_IADs, (void**)&pADsMem);
                        hr = pADsMem->get_ADsPath(&bstrPath);
                        _tprintf(TEXT("  Member:%s\n"),bstrPath);
                        pDispMem->Release();
                        pADsMem->Release();
                    } //Object Fetched.
                } //Member enumerator built.
                hr = ADsFreeEnumerator(pMbrEnum);
                pGrp->Release();
                pMbr->Release();

                pADs->Release();
                pDisp->Release();
            } //Group object bound
        } //Group fetched
    } // Group enumerator built

    hr = ADsFreeEnumerator(pEnum);

    if (pCont)
        pCont->Release();

    return 0;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsEnumerateNext, ADsFreeEnumerator


ADsBuildVarArrayInt

The ADsBuildVarArrayInt function builds a variant array of integers from an array of DWORD values.

HRESULT ADsBuildVarArrayInt(
  LPDWORD  lpdwObjectTypes,
  DWORD    dwObjectTypes,
  VARIANT *pVar
);

Parameters

lpdwObjectTypes
[in] Array of DWORD values.
dwObjectTypes
[in] Number of DWORD entries in the given array.
pVar
[out] Pointer to the resulting variant array of integers.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The array has been built successfully.
E_FAIL
The array could not be created.

For other return values, see ADSI Error Codes.

Remarks

Use the ADsBuildVarArrayInt function to convert the integer array into a variant array of the integers. The following code snippet provides an example.

DWORD dwArray[5]={0,1,2,3,4};
VARINT varArray[5];
HRESULT hr = ADsBuildVarArrayInt(dwArray, 5, varArray);
if (hr = E_FAIL) exit(1);

//You can continue to work with the data in varArray.
. . .

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsBuildVarArrayStr


ADsBuildVarArrayStr

The ADsBuildVarArrayStr function builds a variant array from an array of Unicode strings.

HRESULT ADsBuildVarArrayStr(
  LPWSTR  *lppPathNames,
  DWORD    dwPathNames,
  VARIANT *pVar
);

Parameters

lppPathNames
[in] Array of Unicode strings.
dwPathNames
[in] Number of Unicode entries in the given array.
pVar
[out] Pointer to the resulting variant array.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The array has been built successfully.
E_FAIL
The array could not be created.

For other return values, see ADSI Error Codes.

Remarks

To support Automation, use the ADsBuildVarArrayStr function to convert Unicode strings to a variant array of strings. The following code snippet illustrates how to use this function to convert an object's class names from Unicode strings to a variant array of strings.

HRESULT EnumObject(LPWSTR pszADsPath,
                   LPWSTR * lppClsNames,
                   DWORD dwClsNames)
{
    ULONG ulFetched = 0L;
    IEnumVARIANT * pEnumVar = NULL;
    VARIANT varFilter, varArray[MAX_ADS_ENUM];
    HRESULT hr;
    IADsContainer * pADsContainer = NULL;
    DWORD dwObjects = 0, dwEnumCount=0, i=0;
    BSTR bstrName;
    BOOL fContinue=TRUE;

    hr = ADsGetObject(pszADsPath,
                     IID_IADsContainer,
                     (void**)&pADsContainer);
    if (FAILED(hr)) goto cleanup;

    //Create a string array of class names as search filters.
    VariantInit(&varFilter);
    hr = ADsBuildVarArrayStr(lppClsNames, dwClsNames, &varFilter);
    if (FAILED(hr)) goto cleanup;

    //Apply filters to objects in the container.
    hr = pADsContainer->put_Filter(varFilter);
    if(FAILED(hr)) goto cleanup;

    //Create an enumerator.
    hr = ADsBuildEnumerator(pADsContainer, &pEnumVar);
    if(FAILED(hr)) goto cleanup;

    //Enumerate through the objects and print out their names.
    while(fContinue) {
        IADs* pObject;
        hr = ADsEnumerateNext(pEnumVar, MAX_ADS_ENUM,
                            varArray, &ulFetched);
        if(hr == S_FALSE) fContinue = FALSE;
        dwEEnumCount++;

        for (i=0; i<ulFetched; i++) {
            IDispatch *pDispatch = NULL;
            pDispatch = varArray[I].pDispVal;
            hr = pDispatch->QueryInterface(IID_IADs,
                                        (void**) &pObject);
            if (FAILED(hr)) goto cleanup;

            hr = pObject->get_Name(&bstrName);
            if(FAILED(hr)) goto cleanup;
            printf(" Object name: %S\n",bstrName);

            //We are done with the ADSI object.
            SysFreeString(bstrname);
            pObject->Release();
            pDispatch->Release();
        }
        memset(varArray, 0, sizeof(VARIANT)*MAX_ADS_ENUM);
        dwObjects += ulFetched;
    }
    hr = S_OK;

cleanup:
    if(bstrName) SysFreeString(bstrName);
    if(pEnumvar) ADsFreeEnumerator(pEnumVar);
    if(pADsContainer) pADsContainer->Release();

    return (hr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsBuildVarArrayInt


ADsEncodeBinaryData

The ADsEncodeBinaryData function converts a binary blob of data to the Unicode format suitable to be embedded in a search filter.

HRESULT ADsEncodeBinaryData(
  PBYTE   pbSrcData,
  DWORD   dwSrcLen,
  LPWSTR *ppszDestData
);

Parameters

pbSrcData
A binary blob of data to be converted.
dwSrcLen
Size of the blob in bytes.
ppszDestData
Converted data as a Unicode string.

Return Values

This method supports the standard return values, as well as the following:

E_ADS_BAD_PARAMETER
Parameters are not valid. For example, the length of the source data is zero.
E_OUTOFMEMORY
Memory allocation has failed.
S_OK
Operation is successful.
For other return values, see ADSI Error Codes.

Remarks

In ADSI, search filters must be Unicode strings. Sometimes, a filter contains information that is normally represented by an opaque blob of data. For example, you may want to include an object's security identifier in a search filter, which is of binary data. In this case, you must first call the ADsEncodeBinaryData function to convert the binary data to the Unicode string format. When the data is no longer needed, you should call the FreeADsMem function to free the converted Unicode string (that is, ppszDestData). The following code snippet gives an example of how to use this function.

//Test binary values in filters and use
//a binary filter instead of a string filter in ExecuteSearch.

WCHAR pszBinaryFilter[256] = L"objectSid=";
LPWSTR pszDest = NULL;

BYTE column[100] = {
  0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x15, 0x00,
  0x00, 0x00, 0x59, 0x51, 0xb8, 0x17, 0x66, 0x72, 0x5d, 0x25,
  0x64, 0x63, 0x3b, 0x0b, 0x29, 0x99, 0x21, 0x00 };

hr = ADsEncodeBinaryData (
    column,
    28,
    &pszDest
    );

wcscat( pszBinaryFilter, pszDest );

//Do the search with the pszDest as the filter string. Code omitted.
. . .
//Done with the search and free the converted string.
FreeADsMem( pszDest );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.


ADsEnumerateNext

The ADsEnumerateNext function walks through a specified number of elements from the current cursor position of the enumerator. When the operation succeeds, the function returns the enumerated set of elements in a variant array. The number of returned elements can be smaller than the specified number.

HRESULT ADsEnumerateNext(
  IEnumVARIANT *pEnumVariant,
  ULONG         cElements,
  VARIANT FAR  *pvar,
  ULONG FAR    *pcElementsFetched
);

Parameters

pEnumVariant
[in] Pointer to the IEnumVARIANT interface on the enumerator object.
cElements
[in] Number of elements requested.
pvar
[in] Pointer to the array of elements retrieved.
pcElementsFetched
[out] Actual number of elements retrieved, which can be smaller than the number of elements requested.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The variant array has been populated successfully.
S_FALSE
The call succeeded, but there were no new items.

For other return values, see ADSI Error Codes.

Remarks

The general procedure to enumerate objects in a container involves the following:

  1. Calling ADsBuildEnumerator to create an enumerator object.
  2. Performing enumeration by calling ADsEnumerateNext on the newly created enumerator.
  3. Freeing the enumerator by using ADsFreeEnumerator.

For more information and a code example, see the ADsBuildEnumerator topic.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsBuildEnumerator, ADsFreeEnumerator


ADsFreeEnumerator

The ADsFreeEnumerator function frees an enumerator object that has been created through ADsBuildEnumerator.

HRESULT ADsFreeEnumerator(
  IEnumVARIANT *pEnumVariant
);

Parameters

pEnumVariant
[out] Pointer to the IEnumVARIANT interface on the enumerator object to be freed.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The enumerator object has been freed successfully.
E_ADS_BAD_PARAMETER
The specified enumerator is not valid.
For other return values, see ADSI Error Codes.

Remarks

The general procedure for enumerating objects in a container involves the following:

  1. Calling ADsBuildEnumerator to create an enumerator object.
  2. Performing enumeration by calling ADsEnumerateNext on the newly created enumerator.
  3. Freeing the enumerator by using ADsFreeEnumerator.

For more information and a code example, see the ADsBuildEnumerator topic.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsBuildEnumerator


ADsGetLastError

The ADsGetLastError function retrieves the calling thread's last-error code value. This function behaves in the same way as the Win32 function GetLastError.

HRESULT ADsGetLastError(
  LPDWORD lpError,
  LPWSTR  lpErrorBuf,
  DWORD   dwErrorBufLen,
  LPWSTR  lpNameBuf,
  DWORD   dwNameBufLen
);

Parameters

lpError[out] Pointer to the location that receives the error code.
lpErrorBuf
[out] Pointer to the location that receives the null-terminated string describing the error.
dwErrorBufLen
[in] Size (in characters) of the lpErrorBuf buffer. If the buffer is too small to receive the error string, the string is truncated, but still null-terminated. A buffer of at least 256 bytes is recommended.
lpNameBuf
[out] Pointer to the location that receives the null-terminated string describing the name of the provider that raised the error.
dwNameBufLen
[in] Size (in characters) of the lpNameBuf buffer. If the buffer is too small to receive the error string, the string is truncated, but still null-terminated.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The last error was retrieved successfully.
E_POINTER
One or more of the parameters passed in is bad.
ERROR_BAD_DEVICE
The thread identifier for the current thread could not be found in that table anywhere.
For other return values, see ADSI Error Codes.

Remarks

ADSI errors fall into two types according to the values of their facility code. The standard ADSI error codes have a facility code value of 0x5 and the extended ADSI error codes assume that of FACILITY_WIN32. The error values of the standard and extended ADSI error codes are of the forms of 0x80005xxx and 0x8007xxxx, respectively. Use the HRESULT_FACILITY(hr) macro to determine the type of ADSI errors you are dealing with.

The following code snippet shows how to get Win32 error codes and their descriptions using ADsGetLastError:

if (FAILED(hr))
{
    wprintf(L"An error occurred.\n  HRESULT: %x\n",hr);
    //If facility is Win32, get the Win32 error
    if (HRESULT_FACILITY(hr)==FACILITY_WIN32)
    {
        DWORD dwLastError;
        WCHAR szErrorBuf[MAX_PATH];
        WCHAR szNameBuf[MAX_PATH];
        //Get extended error value.
        HRESULT hr_return =S_OK;
        hr_return = ADsGetLastError( &dwLastError,
                                       szErrorBuf,
                                       MAX_PATH-1,
                                        szNameBuf,
                                       MAX_PATH-1);
        if (SUCCEEDED(hr_return))
        {
             wprintf(L"Error Code: %d\n Error Text: %ws\n Provider: %ws\n", dwLastError, szErrorBuf, szNameBuf);
        }
    }
}

If hr is 80071392, the sample code produces the following output.

Error creating container object.
    HRESULT: 80071392
    Error Code: 8305
    Error Text: 00002071: UpdErr: DSID-030502F1, problem 6005 (ENTRY_EXISTS), data 0
    Provider: LDAP Provider

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

GetLastError, ADsSetLastError


ADsGetObject

The ADsGetObject function binds to an object given its path and a specified interface identifier (IID).

HRESULT ADsGetObject(
  LPWSTR lpszPathName,
  REFIID riid,
  VOID   **ppObject
);

Parameters

lpszPathName
[in] The path name used to bind to the object in the underlying directory service.
riid
[in] Interface identifier for a specified interface on this object.
ppObject
[out] Indirect pointer to the requested Interface.

Return Values

This method supports the standard HRESULT return values, including:

S_OK
Binding to the specified object has been successful.

For other return values, see ADSI Error Codes.

Remarks

A C/C++ client calls the ADsGetObject helper function to bind to an ADSI object. It is equivalent to a Visual Basic® client calling the GetObject function. They both take an ADsPath as input and returns a pointer to the requested interface. By default the binding uses ADS_SECURE_AUTHENTICATION option with the security context of the current logged on user. However, if the authentication fails, the secure bind is downgraded to an anonymous bind, for example, a simple bind without any user credentials.

The following code snippet binds to a user object, "WinNT:://MyDomain/Users/John" through the WinNT provider.

IADsUser    *pUser;
BSTR        bstrName;
HRESULT     hr;
hr = ADsGetObject(L"WinNT://MyDomain/Users/John",
                  IID_IADsUser,
                  (void**) &pUser);

It is possible to bind to an ADSI object with a user credential different from that of the currently logged-on user. This is accomplished by using the ADsOpenObject function.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.


ADsOpenObject

The ADsOpenObject function binds to an ADSI object using explicit username and password credentials.

HRESULT ADsOpenObject(
  LPWSTR          lpszPathName,
  LPWSTR          lpszUserName,
  LPWSTR          lpszPassword,
  DWORD           dwReserved,
  REFIID          riid,
  VOID FAR * FAR *ppObject
);

Parameters

lpszPathName
[in] The path name to use to bind to the object in the underlying directory using the lpszUserName and lpszPassword credentials.
lpszUserName
[in] The user name to supply to the directory service to use for credentials.
lpszPassword
[in] The password to supply to the directory service to use for credentials.
dwReserved
[in] Provider-specific flags. See IADsOpenDSObject.
riid
[in] Interface identifier for the requested interface on this object.
ppObject
[out] Indirect pointer to the requested interface.

Return Values

This method supports the standard HRESULT return values, including:

S_OK
Binding to the specified object has been successful.

For other return values, see ADSI Error Codes.

Remarks

A C/C++ client calls the ADsOpenObject helper function to bind to an ADSI object, using the username and password supplied as credentials for the appropriate directory service. If lpszUsername and lpszPassword are NULL and ADS_SECURE_AUTHENTICATION is set, the security context of the current logged on user will be used.

To work with the WinNT: provider, you can pass in bstrUserName as one of the following strings:

  • the name of a user account, that is, jsmith,
  • the Windows NT® 4.0 style user name, that is, ArcadiaBay\jsmith.

To work with the LDAP provider, such as Active Directory, you can pass in bstrUserName as one of the following strings:

  • the name of a user account, that is, jsmith.
  • down-level user path, that is, AcardiaBay\jsmith.
  • Distinguished Name, that is, CN=jsmith, OUT=Sales, DC=ArcardiaBay, DC=Com.
  • User Principal Name (UPN), that is, jsmith@Arcadiabay.com.

To use UPN, you must assign the appropriate UPN value for the userPrincipalName attribute of the targeted user object.

The following code snippet illustrates how to bind to a directory service object with the user credential of jsmith.

IADs    *pObject;
HRESULT hr;
hr = ADsOpenObject(L"LDAP://CN=Bob,DC=ArcadiaBay,DC=com",
                   "jsmith",
                   "etercespot",
                   ADS_SECURE_AUTHENTICATION,
                   IID_IADs,
                   (void**) &pObject);

This function is equivalent to the IADsOpenDSObject::OpenDsObject method.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

IADsOpenDSObject


ADsSetLastError

The ADsSetLastError sets the calling thread's last-error code value. Directory service providers can use this function to set extended errors. The function saves the error information in a "per thread" data structure. ADsSetLastError behaves the same way as the Win32 function, SetLastError.

VOID ADsSetLastError(
  DWORD  dwErr,
  LPWSTR pszError,
  LPWSTR pszProviderName
);

Parameters

dwErr
[in] The error code that occurred. If this is an error defined by Windows, pszError is ignored. If this is ERROR_EXTENDED_ERROR, it indicates the provider has a network-specific error to report.
pszError
[in] String describing the network-specific error.
pszProviderName
[in] String naming the ADSI provider raising the error.

Return Values

None.

Remarks

In a custom implementation of an ADSI provider, for example, an LDAP provider, you can set an operation error message as follows:

ADsSetLastError(HRESULT_FROM_WIN32(ERROR_DS_OPERATIONS_ERROR),
                L"ERROR_DS_OPERATIONS_ERROR",
                L"LDAP Provider");

The user can use the following code snippet to examine this operation code:

DWORD dwErr;
WCHAR szErr[256];
WCHAR szProv[256];
HRESULT hrGet = ADsGetLastError(dwErr, szErr, szProv);
printf("Error value: %x\n    Message: %ws\n    Provider: %ws\n",
        dwErr, szErr, szProv);

The above code segment will produce the following output for the operations error code set above:

Error value: 80072020
    Message: ERROR_DS_OPERATIONS_ERROR
    Provider: LDAP Provider

If you use ERROR_DS_OPERATIONS_ERROR without invoking the HRESULT_FROM_WIN32 macro when setting the error, the user will get the following output instead:

Error value: 2020
    Message: ERROR_DS_OPERATIONS_ERROR
    Provider: LDAP Provider

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

ADsGetLastError


AllocADsMem

The AllocADsMem function allocates a block of memory of the specified size (in bytes).

LPVOID AllocADsMem(
  DWORD cb
);

Parameters

cb
The amount of memory (in bytes) to be allocated

Return Values

When successful, the function returns a non-NULL pointer to the allocated memory. Otherwise, it returns NULL. Call ADsGetLastError to obtain extended error status. For error code values, see ADSI Error Codes.

Remarks

The following code snippet builds an ADsPath string by concatenating the relative name of an object and the path of its parent. To do so, it first calls AllocADsMem to create a buffer of sufficient length. Error checking is omitted in the example.

HRESULT buildADsPath(BSTR parent, BSTR child, BSTR *pADsPath)
{
    LPWSTR lpADsPath=NULL;
    HRESULT hr = S_OK;
    DWORD dwLen = 0;
    dwLen = wcslen(parent) + wcslen(Name) + 2 + MAX_PATH;

    // create a buffer for the string
    lpADsPath = (LPWSTR)AllocADsMem(dwlen*sizeof(WCHAR));
    if(!lpADspath) return (E_OUTOFMEMORY);

    wcscpy(lpADsPath,parent);
    wcscat(lpADsPath,L"/");
    wcscat(lpADsPath, child);
    hr = ADsAllocString(lpADsPath, pADsPath);

    // release the buffer
    if(lpADsPath) FreeADsMem(lpADsPath);
    return (hr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

FreeADsMem


AllocADsStr

The AllocADsStr function makes a "deep" copy of an specified string. The memory allocated by this function must be released by FreeADsStr.

LPWSTR AllocADsStr(
  LPWSTR pStr
);

Parameters

pStr
Pointer to the string that needs to be allocated and stored.

Return Values

The function returns a non-NULL pointer to the successfully allocated memory for the string. It returns NULL if the operation failed.

Remarks

Use this function to create strings used in ADSI. For example,

#define LDAP 0
#define WINNT 1
LPWSTR namespace;
int prov=0;
. . .
if (prov = WINNT) {
    namespace = AllocADsStr(L"WinNT://");
}
else {
    namespace = AllocADsStr(L"LDAP://");
}
. . .
// when done with namespace, free the string
FreeADsStr(namespace);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

FreeADsStr


FreeADsMem

The FreeADsMem function frees the memory previously allocated by AllocADsMem.

BOOL FreeADsMem(
  LPVOID pMem
);

Parameters

pMem
Pointer to the local memory to be freed.

Return Values

The function returns FALSE if the memory is not allocated by AllocADsMem. Otherwise, it return TRUE.

Remarks

In ADSI, all method-allocated memory must be freed using FreeADsMem. For example,

LPWSTR pszColumn;
m_pSearch->GetFirstRow(hSearch);
printf("Column names are: ");
while(m_pSerach->GetNextColumnName(hSearch, &pszColumn) !=
                                      S_ADS_NOMORE_COLUMNS) {
    printf("%S ", pszColumn);
    // method-allocated memory, pszColumn, must be freed before
    // the pointer variable is used in the next GetnextColumnName call.
    FreeADsMem(pszColumn);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

AllocADsMem


FreeADsStr

The FreeADsStr function frees the memory of a specified string created by AllocADsStr.

BOOL FreeADsStr(
  LPWSTR pStr
);

Parameters

pStr
A string whose memory is to be freed.

Return Values

The function returns TRUE if the memory is freed. Otherwise, it returns FALSE.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

AllocADsStr


ReallocADsMem

The ReallocADsMem function resizes an existing block of memroy.

LPVOID ReallocADsMem(
  LPVOID pOldMem,
  DWORD  cbOld,
  DWORD  cbNew
);

Parameters

pOldMem
Pointer to the existing memory.
cbOld
Size, in bytes, of the existing memory.
cbNew
Size, in bytes, of the memory to be reallocated.

Return Values

When successful, the function returns a pointer to the newly allocated memory. Otherwise it returns NULL.

Remarks

If cbNew is less than cbOld, the existing memory is truncated to fit the new memory size. The following code snippet illustrates how to use ReallocADsMem to enlarge a string.

LPWSTR path = (LPWSTR)AllocADsMem(9*sizeof(WCHAR));
wcscpy(path, L"WinNT://");                 // path becomes "WinNT://"
printf("path = %$\n",path);
path = (LPWSTR)ReallocADsMem(path,
                       9*sizeof(WCHAR),
                      18*sizeof(WCHAR));   // path is still "WinNT://"
wcscat(path, L"localhost");
printf("path = %S\n",path);             // path is "WinNT://localhost"

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

AllocADsMem


ReallocADsStr

The ReallocADsStr function makes a "deep" copy of an existing string.

BOOL ReallocADsStr(
  LPWSTR *ppStr,
  LPWSTR  pStr
);

Parameters

ppStr
Pointer to the destination string.
pStr
Source string.

Return Values

The function returns TRUE if the operation is successful, otherwise FALSE.

Remarks

Use this function to make a deep copy of a string. For example:

LPWSTR theSrc = L"This is my string.";
LPWSTR the Dest;
if (ReallocADsStr(&theDest,theSrc) {
    printf("Copy of %S is  %S\", theSrc, theDest);
}
else {
    printf("Out of Memory.");
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in adshlp.h.

See Also

AllocADsStr, FreeADsStr


ADSI Interfaces

Active Directory™ Service Interfaces (ADSI) supports a rich set of interfaces that can be classified according to the following categories:

  • Core . These interfaces provide the basic object management functions of ADSI objects. The core functions include providing an entry point into a directory store, loading properties into the property cache, and committing changes to the underlying directory.
  • Schema. These interfaces provide methods for managing and extending the directory schema.
  • Property Cache. These interfaces define methods for manipulating properties in the property cache.
  • Persistent Object. These interfaces manipulate persistent data in the namespace of the underlying directory service. ADSI objects implement these types of interfaces to provide access to their persistent data, including user accounts, file shares, organizational hierarchies, and job listings in a print queue.
  • Dynamic Object. These interfaces work with dynamic data in a directory service. Directory objects that are not represented in the underlying directory service implement such interfaces. Examples of dynamic information include commands issued over a network.
  • Security. These interfaces allow an ADSI client to establish its credentials to a server and use security features that the directory service supports, such as the access control list or security descriptors.
  • Non-Automation. These interfaces allow non-Automation clients (for example, C/C++ applications) low-overhead access to directory objects by providing Vtable access to methods for managing and searching directory service objects.
  • Extension. These interfaces allow ADSI clients to extend the functionality of existing ADSI classes to offer customized solutions to directory services.
  • Utility. These interfaces provide advanced helper functions for managing ADSI objects.
  • Data Type. These interfaces provide methods to access ADSI data types.


Alphabetical Listing of ADSI Interfaces

The following table contains an alphabetical listing of all the ADSI interfaces.

Interface | Category
IADs | Core
IADsAccessControlEntry | Security
IADsAccessControlList | Security
IADsAcl | Data Type
IADsBackLink | Data Type
IADsCaseIgnoreList | Data Type
IADsClass | Schema
IADsCollection | Persistent object
IADsComputer | Persistent object
IADsComputerOperations | Dynamic object
IADsContainer | Core
IADsDeleteOps | Utility
IADsDomain | Persistent object
IADsEmail | Data Type
IADsExtension | Extension
IADsFaxNumber | Data Type
IADsFileService | Persistent object
IADsFileServiceOperations | Dynamic object
IADsFileShare | Persistent object
IADsGroup | Persistent object
IADsHold | Data Type
IADsLargeInteger | Data Type
IADsLocality | Persistent object
IADsMembers | Persistent object
IADsNamespaces | Core
IADsNetAddress | Data Type
IADsO | Persistent object
IADsObjectOptions | Utility
IADsOctetList | Data Type
IADsOpenDSObject | Core
IADsOU | Persistent object
IADsPath | Data Type
IADsPathName | Utility
IADsPostalAddress | Data Type
IADsPrintJob | Persistent object
IADsPrintJobOperations | Dynamic object
IADsPrintQueue | Persistent object
IADsPrintQueueOperations | Dynamic object
IADsProperty | Schema
IADsPropertyEntry | Property Cache
IADsPropertyList | Property Cache
IADsPropertyValue | Property Cache
IADsPropertyValue2 | Property Cache
IADsReplicaPointer | Data Type
IADsResource | Dynamic object
IADsSecurityDescriptor | Security
IADsService | Persistent object
IADsServiceOperations | Dynamic object
IADsSession | Dynamic object
IADsSyntax | Schema
IADsTimestamp | Data Type
IADsTypedName | Data Type
IADsUser | Persistent object
IDirectoryObject | Core/Non-Automation
IDirectorySearch | Core/Non-Automation


Interface Property Methods

Many ADSI interfaces are designed to support Automation and, thus, are dual interfaces in that they support client access through both IUnknown and IDispatch interfaces. Non-Automation clients, such as those written in C/C++, resolve method invocation directly, using the IUnknown::QueryInterface, and call the method directly. Automation clients, also known as name-bound clients, such as those written in Visual Basic®, or Visual Basic® Scripting Edition (VBScript), must resolve method invocation indirectly with the help of the dispinterface method.

An ADSI interface supporting Automation defines property methods for retrieving and modifying properties of an object implementing the interface. The property methods have names that have get_ and put_ prepended to the appropriate property names, for example, get_User and put_Name.

Each get_<property> method takes a single parameter as output. This parameter is a method-allocated address of a variable of the property data type. On return, this variable assumes the current value of the requested property. The caller must release the allocated memory of the variable when the property is no longer needed.

Each put_<property> method takes a single parameter as input for the data type of the corresponding property. The parameter holds a new value of the property.

Invocation of the property methods in C++ follows the usual procedure to call the member function of an object, for example:

IADs *pADs;
LPSTR objName;
pADs->Get_Name(&objName);

Invocation of the property methods in Automation clients can be somewhat different. Visual Basic uses the dot notation, for example:

Dim Obj As IADs
Dim objName As String
objName=Obj.Name

All parameters and return types must be of those that the VARIANT data type defines. All methods on a dual interface return an HRESULT value to indicate success or failure.

For more information about getting and setting properties on ADSI objects, see Property Cache.


Interface Implementation of System Providers

ADSI SDK ships with several directory providers, namely, LDAP, WinNT, NDS, and NWCOMPAT. Different providers may choose to implement or not to implement an interface as discussed in this chapter. Of the implemented interfaces, different providers may offer different support mechanism of the properties and methods. Understanding of such provider dependency will help you use the system-supplied providers more effectively. For the information on such provider dependency, see the ADSI System Providers section of this documentation. If you use a third-party provider, consult the documentation supplied by the provider writer.


Core Interfaces

This section describes the following core ADSI interfaces:

  • IADS
  • IADSContainer
  • IADSNamespaces
  • IADSOpenDSObject


IADs

The IADs interface defines the basic object features, that is, properties and methods, of any ADSI object. Examples of ADSI objects include users, computers, services, organization of user accounts and computers, file systems, and file service operations. Every ADSI object must support this interface, which serves to do the following:

  • Provide object identification by name, class, or ADsPath
  • Identify the object's container that manages the object's creation and deletion
  • Retrieve the object's schema definition
  • Load object's attributes to the property cache and commit changes to the persistent directory store
  • Access and modify the object's attribute values in the property cache

The IADs interface is designed to ensure that ADSI objects provide network administrators and directory service providers with a simple and consistent representation of various underlying directory services.

The IADs interface supports the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments the reference count.
Release | Decrements the reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of the object's programmable interface.
GetIDsOfNames | Maps the name of the method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADs property methods | Description
get_Name | Gets the object's relative name.
get_Class | Gets the name of the object's schema class.
get_GUID | Gets the GUID of the object as stored in the underlying directory store.
get_AdsPath | Gets the object's ADsPath that uniquely identifies this object from all others.
get_Parent | Gets the ADsPath string for the parent of the object.
get_Schema | Gets the ADsPath string to the schema class object for this object.
GetInfo | Loads the property values of this object from the underlying directory store.
SetInfo | Persists the changes on this object to the underlying directory store.
Get | Gets the value for a property by name.
Put | Sets the value for a property by name.
GetEx | Gets the value for a single or multi-valued property by name.
PutEx | Sets the value for a single or multi-valued property by name.
GetInfoEx | Loads specific property values of this object from the underlying directory store.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADs Property Methods

The property methods of the IADs interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
AdsPath

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_ADsPath
([out] BSTR *pbstrADsPath);

 The ADsPath string of this object. The string uniquely identifies this object in a networked environment. The object can always be retrieved using this path.
Class

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Class
([out] BSTR *pbstrClass);

 The name of the schema class of this object.
GUID

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_GUID
([out] BSTR *pbstrGUID);

 The globally unique identifier of the directory object. The IADs interface converts the GUID from an octet string, as stored on a directory server, into a string format.
Name

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Name
([out] BSTR *pbstrName);

 The relative name of the object as named within the underlying directory service. This name distinguishes this object from its siblings.
Parent

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Parent
([out] BSTR *pbstrParent);

 The ADsPath string of the parent container. Active Directory™ does not permit the formation of the ADsPath of a given object by concatenating the Parent and Name properties. While this operation might work in some providers, it is not guaranteed to work for all implementations. The ADsPath is guaranteed to be valid and should always be used to retrieve an object's interface pointer.
Schema

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Schema
([out] BSTR *pbstrSchema);

 The ADsPath string of the schema class object of this object

Remarks

In Active Directory the resultant GUID returned from IADs::get_GUID or obj.GUID is a string of hexadecimals that can be used for a fast bind to the object, as shown in the following:

Dim x As IADs
Set x = GetObject("LDAP://servername/<GUID=xxx>")

where xxx is the value returned from the obj.GUID property. For more information, see Using objectGUID to Bind to an Object.

In order to retrieve the Name, AdsPath, or Parent property of an object obtained through the use of its GUID (or SID) as shown above, you must rebind to the object using its distinguished name. This procedure is illustrated in the following code segment.

' Bind to an object using its GUID.
Set obj1 = GetObject("LDAP://<GUID=xxxx>")

' Rebind to the object using its distinguished name
Set obj2 = GetObject("LDAP://" & obj1.Get("distinguishedName"))

Debug.Print obj2.Name
Debug.Print obj2.ADsPath
Debug.Print obj2.Parent

For historical reasons, the WinNT provider returns the GUID property in a slightly different string format that is only human-readable. This is because the WinNT provider does not support the fast bind that uses the object's GUID.

Example Code [Visual Basic]

The following Visual Basic® code snippet illustrates how to retrieve object information using property methods of the IADs interface.

Dim x As IADs
Dim parent As IADsContainer
Dim cls As IADsClass
Dim op As Variant

Set x = GetObject("WinNT://ArcadiaBay/Administrator")
Debug.Print "Object Name: " & x.Name
Debug.Print "Object Path: " & x.ADsPath
Debug.Print "Object Class: " & x.Class

'Get more information on object schema information
Set cls = GetObject(x.Schema)
Debug.Print "Class Name is: " & cls.Name
For Each op In cls.OptionalProperties
    Debug.Print "Optional Property: & op
Next op

Example Code [C++]

The following C++ code snippet shows how to work with the property methods of the IADs interface.

#include <stdio.h>
#include <activeds.h>

int main(int argc, char* argv[])
{
    IADs *pADs;
    IADsUser *pADsUser;
    IADsClass *pCls;
    BSTR bstr;

    HRESULT hr = CoInitialize(NULL);
    if (hr != S_OK) { return 0; }

    hr=ADsGetObject(L"WinNT://ArcadiaBay/Administrator",
                IID_IADsUser,
                (void**) &pADsUser);
    if (hr != S_OK) { return 0; }

    hr = pADsUser->QueryInterface(IID_IADs, (void**) &pADs);
    if( hr !=S_OK) { return 0;}

    pADsUser->Release();

    if( S_OK == pADs->get_Name(&bstr) ) {
        printf("Object Name: %S\n",bstr);
    }

    if( S_OK == pADs->get_ADsPath(&bstr) ) {
        printf("Object path: %S\n",bstr);
    }

    if( S_OK == pADs->get_Class(&bstr) ) {
        printf("Object class: %S\n",bstr);
    }

    hr = pADs->get_Schema(&bstr);
    if ( hr != S_OK) {return 0;}

    hr = ADsGetObject(bstr,IID_IADsClass, (void**)&pCls);
    if ( hr != S_OK) {return 0;}
    if( S_OK == pCls->get_Name(&bstr) ) {
        printf("Class name is %S\n", bstr);
    }

    CoUninitialize();
    return 1;
}

See Also

IADsContainer


IADs::Get

The IADs::Get method retrieves a property of a given name from the property cache. The property can be single-valued, or multi-valued. The property value is represented as either a variant for a single-valued property or a variant array (of VARIANT or bytes) for a property that allows multiple values.

HRESULT IADs::Get(
  BSTR bstrName,
  VARIANT * pvProp
);

Parameters

bstrName
[in] Name of the property, for example, "FullName" of a user.
pvProp
[out] Pointer to a location to receive the value of the property. For a multi-valued property, pvProp is a variant array of VARIANT, unless the property is a binary type. In this latter case, pvProp is a variant array of bytes (VT_U1 or VT_ARRAY). For the property referring to an object, pvProp is a VT_DISPATCH pointer to the object that is referred to.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The property value was retrieved successfully.
E_ADS_PROPERTY_NOT_FOUND
The property is not found in the cache. The property may not have an attribute, or is an invalid property.
E_FAIL
The operation failed.

For other return values, see ADSI Error Codes.

Remarks

The IADs::Get method requires the caller to handle the single- and multi-valued property values differently. Thus, if you know that the property of interest is either single- or multi-valued, you should use the IADs::Get method to obtain the property value. This makes less a demand on ADSI to deal with different property formats and, thus, enhances performance. The following code snippet shows how you, as a caller, can handle single- and multi-valued properties when calling this method.

When a property is uninitialized, calling this method invokes an implicit call to the IADs::GetInfo method. This loads from the underlying directory store the values of the supported properties that have not been set in the cache. Any subsequent calls to IADs::Get deals with property values in the cache only. For more information about the behavior of implicit and explicit calls to IADs::GetInfo, see the discussion of IADs::GetInfo.

You can also use IADs::GetEx to retrieve property values from the property cache. However, the values are returned as a variant array of VARIANTs, regardless of whether they are single- or multi-valued. This means that ADSI makes an extra effort to package the returned property values in consistent data formats. This saves you, as a caller, some efforts to validate the data types when you are not sure whether the returned data has single or multiple values.

Example Code [Visual Basic]

The following Visual Basic code snippet retrieves the security descriptor on an object using IADs::Get.

Dim x As IADs
Dim Desc As IADsSecurityDescriptor
On Error GoTo ErrTest:

Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")

' Single-valued properties:
Debug.Print "Home Phone Number is: " & x.Get("homePhone")

' Some property values represents other ADSI objects.
' Consult your provider documentation
Set Desc = x.Get("ntSecurityDescriptor")

' Multi-valued property, assuming that multiple values have been
' assigned to the "otherHomePhone" properties. Caller must enumerate
' all the available values.
Debug.Print "Other Phone Numbers are: "
otherNumbers = x.Get("otherHomePhone")
For Each homeNum In otherNumbers
  Debug.Print homeNum
Next

Exit Sub

ErrTest:
  Debug.Print Hex(Err.Number)

The following is another Visual Basic code snippet that shows how to work with property values of binary data using IADs::Get and IADs::Put:

Dim oTarget As IADs
Dim Octet(5) As Byte
Dim MultiOctet(2) As Variant
Dim i As Integer, j As Integer

'Set up MultiOctetString
For i = 0 To 2
    For j = 0 To 5
        Octet(j) = CByte(i * j)
    Next j
    MultiOctet(i) = Octet
Next i

'Bind to object and set MultiOctetString
Set oTarget=GetObject("LDAP://CN=SomeUser,CN=Users,DC=ArcadiaBay, DC=COM")
oTarget.Put "multiOctetString", MultiOctet
oTarget.SetInfo

Dim GetOctet As Variant
Dim Temp As Variant

'Read back and printout MultiOctetString
GetOctet = oTarget.Get("multiOctetString")
For i = LBound(GetOctet) To UBound(GetOctet)
    Temp = GetOctet(i)
    For j = LBound(Temp) To UBound(Temp)
        Debug.Print Temp(j)
    Next j
    Debug.Print "----"
Next i

Example Code [C++]

The following C++ code snippet reads attributes with single and multiple values using IADs::Get.

HRESULT hr;
IADs *pUsr=NULL;

CoInitialize(NULL);

////////////////////////////////////
// Bind to a directory object
/////////////////////////////////////
hr = ADsGetObject(L"WinNT://ArcadiaBay/Administrator,user", IID_IADs, (void**) &pUsr );
if ( !SUCCEEDED(hr) ) { return hr; }

///////////////////////////////////
// Get a single value attribute
////////////////////////////////////
VARIANT var;
VariantInit(&var);

hr = pUsr->Get(L"FullName", &var );
if ( SUCCEEDED(hr) )
{
    printf("FullName: %S\n", V_BSTR(&var) );
    VariantClear(&var);
}

if ( pUsr )
{
    pUsr->Release();
}

///////////////////////////////////////////////////////
// Get a multi-valued attribute from a service object
/////////////////////////////////////////////////////////
IADs *pSvc = NULL;

hr = ADsGetObject(L"WinNT://AcadiaBay/Account/Browser,service", IID_IADs, (void**) &pSvc );
if ( !SUCCEEDED(hr) )
{
    return hr;
}

hr = pSvc->Get(L"Dependencies", &var );
if ( SUCCEEDED(hr) )
{
    LONG lstart, lend;
    SAFEARRAY *sa = V_ARRAY( &var );
    VARIANT varItem;

    // Get the lower and upper bound
    hr = SafeArrayGetLBound( sa, 1, &lstart );
    hr = SafeArrayGetUBound( sa, 1, &lend );

    // Now iterate and print the content
    VariantInit(&varItem);
    printf("Getting service dependencies using IADs :\n");
    for ( long idx=lstart; idx < lend; idx++ )
    {
        hr = SafeArrayGetElement( sa, &idx, &varItem );
        printf("%S ", V_BSTR(&varItem));
        VariantClear(&varItem);
    }
    printf("\n");

    VariantClear(&var);
}

// Clean-up
if ( pSvc )
{
    pSvc->Release();
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::GetEx, IADs::Put, IADs::PutEx, Property Cache, IADs::GetInfo


IADs::GetEx

The IADs::GetEx method retrieves from the property cache property values of a given attribute. The returned property values can have single or multiple values. Unlike the IADs::Get method, the property values are returned as a variant array of VARIANT, or a variant array of bytes for binary data. A property with a single value is then represented as an array of a single element

HRESULT IADs::GetEx(
  BSTR bstrName,
  VARIANT * pvProp
);

Parameters

bstrName
[in] Name of the property, for example "homeNumber".
pvProp
[out, retval] Pointer to a location to receive the value(s) of the property.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The property value was retrieved successfully.
E_ADS_PROPERTY_NOT_FOUND
The property is not found in the property cache. The property may not have attributes, or is an invalid property.
E_FAIL
The operation failed.

For other return values, see ADSI Error Codes.

Remarks

The IADs::Get and IADs::GetEx methods return a different variant structure for a single-valued property value. If the property is a string, IADs::Get will return a variant of string (VT_BSTR), whereas IADs::GetEx will return a variant array of a VARIANT type string with a single element. Thus, if you are not sure that a multi-valued attribute will return a single value or multiple values, you should use IADs::GetEx. As it does not require you to validate the result's data structures, you may want to use IADs::GetEx to retrieve a property of which you are not sure whether it is single-valued or multi-valued. The following table compares the difference in calling the two methods.

IADs::Get version IADs::GetEx version
Dim x as IADs

otherNumbers = x.Get("otherHomePhone")
If VarType(otherNumbers) = vbString Then
  Debug.Print otherNumbers
Else
  For Each homeNum In otherNumbers
    Debug.Print homeNum
  Next
End If

Dim x as IADs

otherNumbers = x.GetEx("otherHomePhone")
For Each homeNum In otherNumbers
  Debug.Print homeNum
Next

Like the IADs::Get method, IADs::GetEx makes an implicit call to IADs::GetInfo against an uninitialized property cache. For more information on implicit and explicit calls to IADs::GetInfo, see the IADs::GetInfo topic in this document.

Example Code [Visual Basic]

The following Visual Basic code example shows how to use IADs::GetEx to retrieve properties from an object.

Dim x As IADs
On Error GoTo ErrTest:

Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=AcadiaBay,DC=com")

' Single value property
Debug.Print "Home Phone Number is: "
phoneNumber = x.GetEx(""homePhone")
For Each homeNum in phoneNumber
    Debug.Print homeNum
Next

' Multi valued property
Debug.Print "Other Phone Numbers are: "
otherNumbers = x.GetEx("otherHomePhone")
For Each homeNum In otherNumbers
    Debug.Print homeNum
Next
Exit Sub

ErrTest:
Debug.Print Hex(Err.Number)

Example Code [C++]

The following C++ code snippet retrieves the "homePhone" property values using IADs::GetEx.

IADs *pADs = NULL;

hr = ADsGetObject(L"WinNT://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=Com", IID_IADs, (void**) &pADs );
if ( !SUCCEEDED(hr) ) { return hr;}

hr = pADs->GetEx(L"homePhone", &var );
if ( SUCCEEDED(hr) )
{
    LONG lstart, lend;
    SAFEARRAY *sa = V_ARRAY( &var );
    VARIANT varItem;

    // Get the lower and upper bound
    hr = SafeArrayGetLBound( sa, 1, &lstart );
    hr = SafeArrayGetUBound( sa, 1, &lend );

    // Now iterate and print the content
    VariantInit(&varItem);
    printf("Getting Home Phone using IADs::Get :\n");
    for ( long idx=lstart; idx < lend; idx++ )
    {
        hr = SafeArrayGetElement( sa, &idx, &varItem );
        printf("%S ", V_BSTR(&varItem));
        VariantClear(&varItem);
    }
    printf("\n");

    VariantClear(&var);
}

// Clean-up
if ( pADs )
{
    pADs->Release();
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::Get, IADs::Put, IADs::PutEx, Property Cache


IADs::GetInfo

The IADs::GetInfo method loads into the property cache values of the supported properties of this ADSI object from the underlying directory store.

HRESULT IADs::GetInfo(void);

Return Values

This method supports the standard return values, as well as the following:

S_OK
The current set of property values for this object have been refreshed.
E_ADS_OBJECT_UNBOUND
The specified ADSI object is not bound to the remote resource.

For other return values, see ADSI Error Codes.

Remarks

The IADs::GetInfo function is called, implicitly or explicitly, to initialize or refresh the property cache. This amounts to fetching those property values of supported properties from the underlying directory store. An uninitialized property cache is not necessarily empty. In fact, you can call IADs::Put or IADs::PutEx to put a value into the property cache for any supported property and the cache remains uninitialized. An explicit call to IADs::GetInfo will load or reload the entire property cache, overwriting all the cached property values. But an implicit call will load only those properties that have not been set in the cache. Thus, you should always call IADs::GetInfo explicitly if you want to obtain the most up-to-date property values of the ADSI object. The following code snippet illustrates such subtleties.

Because an explicit call to IADs::GetInfo overwrites all the values in the property cache, any change made to the cache will be lost if an IADs::SetInfo was not invoked before IADs::GetInfo.

For an ADSI container object, IADs::GetInfo caches only the property values of the container, but not those of the child objects.

It is important to emphasize the differences between the IADs::Get and IADs::GetInfo methods. The former returns values of a given property from the property cache whereas the latter loads all the supported property values into the property cache from the underlying directory store. The following Visual Basic code snippet illustrates these differences.

Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")
                                     'The first IADs::Get will call
                                     'GetInfo implicitly
Debug.Print x.Get("homePhone")       'assume value is '999-9999'
x.Put "homePhone", "868-4449"        'Put with no commit(SetInfo)
Debug.Print x.Get("homePhone")       'Value='868-4449' from the cache
x.GetInfo                            'Refresh the cache, get the data
                                     'from the directory
Debug.Print x.Get("homePhone")       'value will be '999-9999'

For performance enhancement, you can make explicit calls to IADs::GetInfoEx to refresh specific properties. This function always overwrites any previously cached values of the specified properties.

Example Code [Visual Basic]

The following Visual Basic code snippet makes use of a computer object served by the WinNT provider. The supported properties include Owner ("Owner"), OperatingSystem ("Windows NT"), OperatingSystemVersion ("4.0"), Division ("ArcadiaBay"), ProcessorCount ("Uniprococessor Free"), Processor ("x86 Family 6 Model 5 Stepping 1"). The default values are shown in parentheses for the purpose of illustration.

Dim pList As IADsPropertyList
Dim pEntry As IADsPropertyEntry
Dim pValue As IADsPropertyValue

Set pList = GetObject("WinNT://localhost,computer")

' pList now represents an uninitialized empty property cache
pList.Put "Owner" "buddy"  ' property cache remain uninitialized
                           ' but with one property value.
count = pList.PropertyCount   ' count = 1.
MsgBox "Number of property found in the property cache: " & count

v = pList.Get("Division")   ' pList.GetInfo is called implicitly
ShowPropertyCache           ' This will display "buddy" for Owner,
                                      "ArcadiaBay" for Division,
                               "Windows NT" for OperatingSystem, and so on

pList.GetInfo                ' refreshes the entire cache, overwriting
                            " "buddy" for the Owner property.
ShowPropertyCache           ' This will display "Owner" for Owner,
                                      "ArcadiaBay" for Division,
                               "Windows NT" for OperatingSystem, and so on

Private Sub ShowPropertyCache()
    For I = 0 To pList.PropertyCount-1
       Set pEntry = pList.Item(I)
       Debug.Print pEntry.Name
       For Each v In pEntry.Values
           Set pValue = v
           Debug.Print "   " & pvalue.CaseIgnoreString
       Next
    Next
End Sub

Example Code [C++]

The following C++ code snippet highlights the effect of Get and GetInfo. For brevity error checking is omitted.

IADs *pADs;
IADsPropertyList *pList;
BSTR bstr;
VARIANT var;
HRESULT hr;

hr = ADsGetObject(L"WinNT://kding1,computer",
                  IID_IADsPropertyList,
                  (void**)&pList);

// get the number of property entries, should be zero.
long pCount;
hr = pList->get_PropertyCount(&pCount);
printf("    prop count = %d\n",pCount);     // 0 for empty cache.

hr = pList->QueryInterface(IID_IADs, (void**)&pADs);

VariantInit(&var);

// Set "Owner=Buddy" in the property cache.
V_BSTR(&var) = SysAllocString(L"Buddy");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"Owner",var);
VariantClear(&var);

//This time the number of property entries should read one (1).
hr = pList->get_PropertyCount(&pCount);
printf("    prop count = %d\n",pCount);    // 1 for what we have set.

// The following Get will invoke GetInfo implicitly, but
// whatever is already in the cache (that is, "Owner=Buddy") will
// remain intact.
hr = pADs->Get(L"Division",&var);
printf("    division   = %S\n", V_BSTR(&var));
VariantClear(&var);

hr = pADs->Get(L"Owner", &var);
printf("    owner      = %S\n", V_BSTR(&var));  // Owner = Buddy
VariantClear(&var);

// The following GetInfo call refreshes the entire prop cache.
// Now Owner is no longer "Buddy", but the value stored in the
// persistent store, for example, "Bob".
hr = pADs->GetInfo();

hr = pADs->Get(L"Owner", &var);
printf("    owner      = %S\n", V_BSTR(&var));  // Owner = Bob
VariantClear(&var);

// ...

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::GetInfoEx, IADs::Get, IADs::GetEx, Property Cache


IADs::GetInfoEx

The IADs::GetInfoEx method loads the values of specified properties of the ADSI object from the underlying directory store into the property cache.

HRESULT IADs::GetInfoEx(
  VARIANT vProperties,
  LONG lnReserved
);

Parameters

vProperties
[in] Array of BSTR entries that list the properties to load into the Active Directory property cache. Each property name must match one in this object's schema class definition.
lnReserved
[in] Reserved for future use. Must be zero (0).

Return Values

This method supports the standard return values, as well as the following:

S_OK
The current set of property values for this object have been refreshed.
E_ADS_OBJECT_UNBOUND
The specified ADSI object is not bound to the remote resource.

For other return values, see ADSI Error Codes.

Remarks

The IADs::GetInfoEx method overwrites any previously cached values of the specified properties with those in the directory store. Thus, any change made to the cache will be lost if an IADs::SetInfo was not invoked before the call to IADs::GetInfoEx.

Use IADs::GetInfoEx to refresh values of the selected property in the property cache of an ADSI object. Use IADs::GetInfo to refresh all the property values.

For an ADSI container object, IADs::GetInfoEx caches only the property values of the container, but not those of the child objects.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use the IADs::GetInfoEx to obtain values of the selected properties, assuming that the desired property values can be found in the directory.

Dim x As IADs
Set x = GetObject("LDAP://CN=JSmith,CN=Users,DC=ArcadiaBay,DC=com")

'Retrieve givenName and sn from the underlying directory storage.
'Cache should have givenName and sn values.
x.GetInfoEx Array("givenName", "sn"), 0
Debug.Print x.Get("givenName")  ' Property is in the cache
Debug.Print x.Get("sn")         ' Property is in the cache

'If the "homePhone" property is not in the cache (in the next line),
'GetInfo will be called implicitly.
Debug.Print x.Get("homePhone")

Example Code [C++]

The following C++ code snippet shows how to use the IADs::GetInfoEx to obtain values of the selected properties, assuming that the desired property values can be found in the directory. For brevity, error checking has been omitted.

IADs *pADs;
VARIANT var;
HRESULT hr;

hr = ADsGetObject(L"WinNT://kding1,computer",
                  IID_IADs,
                  (void**)&pADs);

VariantInit(&var);

// Fetch "Owner" and "Division" attribute values.
LPWSTR pszAttrs[] = { L"Owner", L"Division" };
DWORD dwNumber = sizeof( pszAttrs ) /sizeof(LPWSTR);
hr = ADsBuildVarArrayStr( pszAttrs, dwNumber, &var );
hr = pADs->GetInfoEx(var, 0);
VariantClear(&var);

hr = pADs->Get(L"Division",&var);
printf("    division   = %S\n", V_BSTR(&var));
VariantClear(&var);
hr = pADs->Get(L"Owner", &var);
printf("    owner      = %S\n", V_BSTR(&var));
VariantClear(&var);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::GetInfo, IADs::SetInfo, Property Cache


IADs::Put

The IADs::Put method lets a client set new values to a specified property in the property cache.

HRESULT IADs::Put(
  BSTR bstrName,
  VARIANT vProp
);

Parameters

bstrName
[in] Name of the property, for example "FullName".
vProp
[in] Values of the property.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The property value has been set successfully.
E_ADS_CANT_CONVERT_DATATYPE
The ADSI data type cannot be converted to or from the native data type of the underlying directory.

For other return values, see ADSI Error Codes.

Remarks

The assignment of the new property values, performed by IADs::Put, takes place in the property cache only. To propagate the changes to the directory store, you must call IADs::SetInfo on the object after calling IADs::Put. To manipulate the property values beyond the simple assignment, use IADs::PutEx, which allows you to append a value to or remove one from an existing array of attribute values.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use the IADs::Put method.

Dim x As IADs
Set x = GetObject("LDAP://CN=JSmith,CN=Users,DC=ArcadiaBay, DC=Com")
x.Put "givenName", "John"
x.Put "sn", "Smith"
x.SetInfo    'commit to the directory

Example Code [C++]

The following C++ code snippet uses IADs::Put to set some user attributes.

HRESULT hr;
IADs *pADs=NULL;
LPWSTR pszADsPath = L"LDAP://CN=JSmith, CN=Users, DC=ArcadiaBay, DC=com";

CoInitialize(NULL);

//////////////////////////////////
// Modifying attributes using IADs
//////////////////////////////////
hr = ADsGetObject(pszADsPath, IID_IADs, (void**) &pADs );

if (!SUCCEEDED(hr) ) { return hr; }

VARIANT var;
VariantInit(&var);

// Setting the first name
V_BSTR(&var) = SysAllocString(L"John");
V_VT(&var) = VT_BSTR;
hr = pADs->Put( L"givenName", var );

// Setting the last name
VariantClear(&var);
V_BSTR(&var) = SysAllocString(L"Smith");
V_VT(&var) = VT_BSTR;
hr = pADs->Put( L"givenName", var );
VariantClear(&var);

// Other Telephones
LPWSTR pszPhones[] = { L"425 844 1234", L"425 924 4321" };
DWORD dwNumber = sizeof( pszPhones ) /sizeof(LPWSTR);
hr = ADsBuildVarArrayStr( pszPhones, dwNumber, &var );
hr = pADs->Put( L"otherTelephone", var );
VariantClear(&var);

// Commit the change to the directory
hr = pADs->SetInfo();
pADs->Release();

if (!SUCCEEDED(hr) )
{

return hr;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::Get, IADs::GetEx, IADs::PutEx, Property Cache


IADs::PutEx

The IADs::PutEx method is used to modify cached values of a specified property with a greater degree of control. For example, you can append a value to the existing ones of a multi-valued property, change all the values at once, remove the specified values from the set, or delete all the values at once.

HRESULT IADs::PutEx(
  LONG lnControlCode,
  BSTR bstrName,
  VARIANT vProp
);

Parameters

lnControlCode
[in] Control code to indicate the mode of modification: Append, Replace, Remove, and Delete. The values are specified in ADS_PROPERTY_OPERATION_ENUM.
bstrName
[in] Name of the property for example "FullName".
vProp
[in] Value or values of the property. The property value(s) is (are) returned as a variant array of VARIANT, or a variant array of bytes for binary data. A property with a single value is then represented as an array of a single element

Return Values

This method supports the standard return values, as well as the following:

S_OK
The property values have been set successfully.
E_ADS_BAD_PARAMETER
The supplied dwControlCode parameter is invalid.
E_ADS_CANT_CONVERT_DATATYPE
The ADSI data type cannot be converted to or from the native data type of the underlying directory.

For other return values, see ADSI Error Codes.

Remarks

After calling IADs::PutEx, you must call IADs::SetInfo to commit the changes in the cache to the underlying directory store , if you wish to make the changes persistent.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use IADs::PutEx with the control code to modify values of a named property.

Dim x As IADs
Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")
'----------------------------------------------
' Assume the otherHomePhoneNumber has the following values:
' 111-1111, 222-2222
'----------------------------------------------

' Adding a value
x.PutEx ADS_PROPERTY_APPEND, "OtherhomePhone", Array("333-3333")
x.SetInfo              'Now the values are 111-1111,222-222,333-3333

' deleting two values
x.PutEx ADS_PROPERTY_DELETE, "OtherHomePhone", Array("111-1111", "222-2222")
x.SetInfo              'Now the values are 333-3333

' changing the remaining value
x.PutEx ADS_PROPERTY_UPDATE, "OtherHomePhone", Array("888-8888", "999-9999")
x.SetInfo              'Now the values are 888-8888,999-9999

'Deleting the value.
x.PutEx ADS_PROPERTY_CLEAR, "OtherHomePhone",  vbNullString
x.SetInfo              'Now the property has no value

Example Code [C++]

The following C++ code snippet uses IADs::PutEx to set some user attributes. For brevity, error checking is omitted.

HRESULT hr;
IADs *pADs=NULL;
LPWSTR pszADsPath = L"LDAP://CN=Administrator, CN=Users, DC=ArcadiaBay, DC=com";

CoInitialize(NULL);

hr = ADsGetObject(pszADsPath, IID_IADs, (void**) &pADs );

VARIANT var;
VariantInit(&var);

LPWSTR pszPhones[] = { L"111-1111", L"222-2222" };
DWORD dwNumber = sizeof( pszPhones ) /sizeof(LPWSTR);
hr = ADsBuildVarArrayStr( pszPhones, dwNumber, &var );
hr = pADs->Put( L"OtherHomePhone", var );
VariantClear(&var);
hr = pADs->SetInfo();   // now the phone list is (111-1111, 222-2222)

// Append another number to the list.
LPWSTR pszAddPhones[]={L"333-3333"};
hr = ADsBuildVarArrayStr( pszAddPhones, 1, &var);
hr = pADs->PutEx( ADS_PROPERTY_APPEND, L"OtherHomePhone", var );
hr = pADs->SetInfo();   //the list becomes
                        // (111-1111, 222-2222, 333-3333)
VariantClear(&var);

hr = ADsBuildVarArrayStr( pszPhones, dwNumber, &var);
hr = pADs->PutEx( ADS_PROPERTY_DELETE, L"OtherHomePhone", var);
hr = pADs->SetInfo();  // the list becomes (333-3333)

pszPhones[0] = L"888-8888";
pszPhones[1] = L"999-9999";
hr = ADsBuildVarArrayStr( pszPhones, dwNumber, &var);
hr = pADs->PutEx( ADS_PROPERTY_UPDATE, L"OtherHomePhone", var);
hr = pADs->SetInfo();  // the list becomes (888-8888, 999-9999)

VariantClear(&var);
V_VT(&var)=VT_NULL;
hr = pADs->PutEx( ADS_PROPERTY_CLEAR, L"OtherHomePhone", NULL);
hr = pADs->SetInfo();  // the list is now empty

hr = CoUninitialize();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::Get, IADs::GetEx, IADs::Put, Property Cache


IADs::SetInfo

The IADs::SetInfo method saves the cached property values of the ADSI object to the underlying directory store.

HRESULT IADs::SetInfo(void);

Return Values

This method supports the standard return values, including S_OK for a successful operation. For other return values, see ADSI Error Codes.

Remarks

It is important to emphasize the differences between the IADs::Put (or IADs::PutEx) and IADs::SetInfo methods. The former sets (or modifies) values of a given property in the property cache whereas the latter propagates the changes from the property cache into the underlying directory store. Thus, any property value changes made through IADs::Put (or IADs::PutEx) will be lost when IADs::GetInfo (or IADs::GetInfoEx) is invoked before IADs::SetInfo is called.

Because IADs::SetInfo sends data across networks, you should try to minimize the usage of this method whenever possible. This helps to reduce the number of trips that a client makes to the server. For example, you should commit all (or most) the changes of the properties from the cache to the persistent store in one batch. You should avoid calling IADs::SetInfo after each call to IADs::Put (or IADs::PutEx). The following code snippets illustrate what is recommended and what is not.

Case 1: Recommended

Dim obj as IADs

obj.Put(prop1,val1)
obj.Put(prop2.val2)
obj.Put(prop3.val3)
obj.SetInfo

Case 2: Not recommended

obj.Put(prop1,val1)
obj.SetInfo
obj.Put(prop2.val2)
obj.SetInfo
obj.Put(prop3.val3)
obj.SetInfo

When used with IADs::PutEx, IADs::SetInfo passes the operational requests that are specified by control codes, such as ADS_PROPERTY_UPDATE or ADS_PROPERTY_CLEAR, to the underlying directory store.

Example Code [Visual Basic]

The following Visual Basic code snippet uses the IADs::SetInfo method to save the property value of a user to the underlying directory.

Set x = GetObject("LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com")
'
' update values in cache
'
x.Put "sn", "Smith"
x.Put "givenName", "John"
x.Put "street", "123 Main Street"
x.Put "l", "Bellevue"
x.Put "st", "Washington"
'
' commit changes to the directory.
x.SetInfo

Example Code [C++]

The following C++ code snippet updates property values in the property cache and commits the change to the directory store using IADs::SetInfo. Error checking is omitted.

IADs *pAds;
VARIANT var;
HRESULT hr;
LPWSTR path=L"LDAP://CN=Administrator,CN=Users,DC=ArcadiaBay,DC=com";
hr = ADsGetObject( path, IID_IADs, (void**) pADs);

// update values in cache
V_BSTR(&var) = SysAllocString(L"Smith");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"sn", var );
VariantClear(&var);

V_BSTR(&var) = SysAllocString(L"John");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"givenName", var );
VariantClear(&var);

V_BSTR(&var) = SysAllocString(L"123 Main Street");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"street", var );
VariantClear(&var);

V_BSTR(&var) = SysAllocString(L"Bellevue");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"l", var );
VariantClear(&var);

V_BSTR(&var) = SysAllocString(L"Washington");
V_VT(&var) = VT_BSTR;
hr = pADs->Put(L"st", var );
VariantClear(&var);

// Commit changes to the directory store.
hr = pADs->SetInfo();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADs::GetInfo


IADsContainer

The IADsContainer interface defines methods to enable an ADSI container object to create, delete and manage contained ADSI objects. Container objects are used both to represent the hierarchical directory trees, such as a file system, and to organize the directory hierarchy. An example of ADSI container objects is the namespaces object that contains provider-specific top-level directory objects representing the directory trees. Other examples include a network domain object containing computer, among other things. A computer object itself can contain other objects, such as User, Group, Service, FileService and PrintQueue.

You can use the IADsContainer interface to either enumerate contained objects or manage these objects' life-cycle. An example would be to recursively navigate down a directory tree. By querying the IADsContainer interface on an ADSI object, you can determine if the object has any children. If the interface is not supported, the object is a leaf. Otherwise, it is a container. You can continue this process for the newly found container objects. To create, copy, or delete an object, you typically make the request to the container object to perform the task.

The IADsContainer interface exposes the following methods and property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments the reference count.
Release | Decrements the reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of the object's programmable interface.
GetIDsOfNames | Maps the name of the method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsContainer property methods | Description
get_Count | Gets the number of directory objects in the container.
get__NewEnum | Get the interface on an enumerator object.
Get/put_Filter | Gets and sets the filter on the schema classes to use for an enumeration.
Get/put_Hints | Gets and sets properties to load.
GetObject | Gets interface on a named object.
Create | Requests creation of specified object.
Delete | Deletes a specified object.
CopyHere | Copies a specified object within a directory service.
MoveHere | Moves a specified object within a directory service.

Remarks

A much simpler way to determine if an object is a container or not is to invoke the IADsClass::get_Container method on the object's schema class object. This method returns TRUE if the object is a container, FALSE otherwise.

Example Code [Visual Basic]

The following Visual Basic code snippet determines if an ADSI object is a container.

Dim obj As IADs
Dim cls As IADsClass
Set obj = GetObject("WinNT://myComputer,computer")
Set cls = GetObject(obj.Schema)
If (cls.Container = TRUE) then
    MsgBox "The object is a container."
Else
    MsgBox "The object is a leaf."
End If

Example Code [C++]

The following C++ code snippet determines if an ADSI object is a container. For brevity, error checking is omitted.

IADs *pADs;
ADsGetObject(L"WinNT://myComputer,computer", IID_IADs, (void**)&pADs);

BSTR bstr;
pADs->get_Schema(&bstr);
IADsClass *pCls;
ADsGetObject(bstr, IID_IADsClass, (void**)&pCls);
pADs->Release();
SysFreeString(bstr);

VARIANT_BOOL isContainer;
pCls->get_Container(&isContainer);

if(isContainer)
    printf("Object is a container.\n");
else
    printf("Object is not a container.\n");

pCls->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsContainer Property Methods

The property methods of the IADsContainer interface get or set the properties described in the following table. For general discussions of property method, see Interface Property Methods.

Properties in Vtable Order

Property Description
Count

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Count
([out] LONG *plCount);

 The number of items in the container. When Filter (see the next entry) is set, count returns only the number of filtered items.
Filter

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Filter
([out] VARIANT *pvFilter);

HRESULT put_Filter
([in] VARIANT vFilter);

 The filter used to select object classes in a given enumeration. This is a variant array, each element of which is the name of a schema class. If Filter is not set or set to empty, all objects of all classes are retrieved by the enumerator.
Hints

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Hints
([out] VARIANT *pvHints);

HRESULT put_Hints
([in] VARIANT vHints);

 A variant array of BSTR strings. Each element identifies the name of a property found in the schema definition. The vHints parameter allows the client to indicate which attributes to load for each enumerated object. Such information may be used to optimize network access. The exact implementation, however, is provider-specific.

Remarks

The enumeration processes beneath IADsContainer::get__NewEnum and IADsContainer::get_Count are carried out against the contained objects in the cache. When a container contains a large number of objects, the performance may be an issue. To enhance the performance, you should turn off the cache, set up an appropriate page size, and use the IDirectorySearch interface. For this reasons, the get_Count property is not supported in the LDAP provider supplied by Microsoft®.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how the property methods of IADsContainer can be used.

Dim cont as IADsContainer
Dim usr as IADsUser

Set cont = GetObject("LDAP://OU=Sales, DC=ArcadiaBay, DC=COM")
cont.Hints = Array("adminDescription") ' Load only this attribute (optional)
Debug.Print cont.Get("adminDescription")
'We want only users.
cont.Filter = Array("user")
For Each usr In cont
  Debug.Print usr.Name
Next

Example Code [C++]

The following C++ code snippet shows how the property methods of IADsContainer can be used. For brevity, error checking is omitted.

IADsContainer *pCont;
IADs *pChild;
IADs *pADs;

HRESULT hr = ADsGetObject(L"LDAP://OU=Sales,DC=ArcadiaBay,DC=COM",
                          IID_IADsContainer,
                          (void**)&pCont);
LPWSTR pszArray[] = { L"adminDescription" };
DWORD dwNumber = sizeof(pszArray)/sizeof(LPWSTR);
hr = ADsBuildVarArrayStr( pszArray, dwNumber, &var);
hr = pCont->put_Hints( var );
VariantClear(&var);

hr = pCont->QueryInterface(IID_IADs, (void**)pADs);

hr = pADs->Get(L"adminDescription", var);

LPWSTR pzsUsers = {L"user"};
dwNumber = sizeof(pszUsers)/sizeof(LPWSTR);
hr = ADsBuildArrayStr(pszeUsers, dwNumber, &var);
hr = pCont->put_Filter( var );
VariantClear(&var);

// Enumerate user objects in the container
IEnumVARIANT *pEnum = NULL;
hr = ADsBuildEnumerator(pCont, &pEnum);
pCont->Release();    // no longer needed once we have users.

ULONG lFetch;
VariantClear(&var);
while (SUCCEEDED(ADsEnumerateNext(pEnum, 1, &var, &lFetch)) &&
                      lFetch==1) {
    hr = V_DISPATCH(&var)->QueryInterface(IID_IADs, (void**)&pChild)
    if(SUCCEEDED(hr)) {
        BSTR bstrName;
        pChild->get_Name(&bstrName);
        printf("  %S\n", bstrName);
        SysFreeString(bstrName);
        pChild->Release();
    }
    VariantClear(&var);
}


IADsContainer::CopyHere

The IADsContainer::CopyHere method creates a new copy of the specified directory object in this container.

HRESULT IADsContainer::CopyHere(
  BSTR bstrSourceObject,
  BSTR bstrNewName,
  IADs ** ppNewObject
);

Parameters

bstrSourceObject
[in] The ADsPath of the object to be copied.
bstrNewName
[in] Optional name of the new object within the container.
ppNewObject
[out] Indirect pointer to the IADs interface on the copied object.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Remarks

The destination container must be in the same directory service as the source container. Copying objects across directory service implementations is not permitted.

Of the Microsoft-supplied providers, only the NDS provider supports a non trivial implementation of this method. Other providers simply return the E_NOTIMPL error message.

Example Code [Visual Basic]

The following Visual Basic code snippet copies a user object, "JohnSmith", to a new user object, "JaneSmith", within the same organization unit. This example shows a short cut to create a new user.

Dim obj as IADsContainer
Dim newusr as IADsUser
Set obj = GetObject("NDS://myTree/O=MyCompany/OU=Sales")
Set newusr = obj.CopyHere("NDS://myTree/O=MyCompany/OU=Sales/CN=JohnSmith", "JaneSmith")
newuser.EmployeeID = 151
newuser.FirstName = "Jane"
newuser.setInfo

Example Code [C++]

The following C++ code snippet copies a user object, "JohnSmith", to a new user object, "JaneSmith", within the same organization unit. This example shows a short cut to create a new user.

IADsContainer *pCont;
IADsUser *pUser;
VARIANT var;
HRESULT hr;

CoInitialize(NULL);

// bind to an organization object
hr = ADsGetObject(L"NDS://myTree/O=MyCompany/OU=Sales",
                  IID_IADsContainer,
                  (void**)&pCont);

// create Jane Smith from John Smith.
pCont->CopyHere(L"NDS://myTree/O=MyCompany/OU=Sales/CN=JohnSmith",
                L"JaneSmith",
                (IADs**)&pUser);
VariantInit(&var);
V_BSTR(&var) = SysAllocString(L"151");
V_VT(&var) = VT_BSTR;
pUser->put_EmployeeID(var);

VariantClear(&var);
V_BSTR(&var)=SysAllocString(L"Jane");
pUser->put_FirstName(var);

// commit changes to the directory store.
pUser->SetInfo();

CoUninitialize();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsContainer::MoveHere


IADsContainer::Create

The IADsContainer::Create method sets up a request to create a directory object of the specified schema class and a given name in the container. The object is not made persistent until IADs::SetInfo is called on the new object. This allows for setting mandatory properties on the new object.

HRESULT IADsContainer::Create(
  BSTR bstrClass,
  BSTR bstrRelativeName,
  IDispatch ** ppNewObject
);

Parameters

bstrClass
[in] Name of the schema class object to be created. The name is that returned from the IADs::get_Schema method.
bstrRelativeName
[in] Relative name of the object as it is known in the underlying directory and identical to the one retrieved through the IADs::get_Name method.
ppNewObject
[out] Indirect pointer to the IDispatch interface on the newly created object.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Example Code[Visual Basic]

The following Visual Basic code snippet illustrates how to create a user object using IADsContainer::Create with IADs::SetInfo.

Dim cont as IADsContainer
Dim usr as IADsUser

Set cont = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
Set usr = cont.Create("user", "CN=jsmith")
usr.Put "samAccountName", "jsmith"
usr.SetInfo

Example Code[C++]

The following C++ code snippet shows how to create a user object.

HRESULT hr;
IADsContainer *pCont = NULL;

CoInitialize(NULL);

hr = ADsGetObject( L"WinNT://myMachine",
                   IID_IADsContainer,
                   (void**) &pCont );
if (!SUCCEEDED(hr) ) {return hr;}

IADs *pADs=NULL;
IDispatch *pDisp=NULL;
hr = pCont->Create(L"user", L"JSmith", &pDisp );
pCont->Release();

if( !SUCCEEDED(hr) ) { return hr; }

hr = pDisp->QueryInterface( IID_IADs, (void**) &pADs );
pDisp->Release();

if ( !SUCCEEDED(hr) ) { return 0;}

pADs->SetInfo(); // Commit
pADs->Release(); // Release

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsContainer::Delete


IADsContainer::Delete

The IADsContainer::Delete method deletes the specified directory object in this container from the underlying directory store.

HRESULT IADsContainer::Delete(
  BSTR bstrClass,
  BSTR bstrRelativeName
);

Parameters

bstrClass
[in] Name of the schema class object to be deleted. The name is that returned from the IADs::get_Schema method.
bstrRelativeName
[in] Name of the object as it is known in the underlying directory and identical to the one retrieved through the IADs::get_Name method.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Remarks

When using the IADsContainer::Delete method to delete an object in C/C++ applications, you should release the interface pointers to that object as well. This is because the method removes the object from the underlying directory immediately, but leave intact any interface pointers held by the application, that is, in memory, for the deleted object. If they are not released, confusing situations might arise in which you may call IADs::Get and IADs::Put on the deleted object without error, but will get an error when you call IADs::SetInfo or IADs::GetInfo on the deleted object.

Example Code [Visual Basic]

The following Visual Basic code snippet deletes a user object from the container in Active Directory.

Set cont = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
cont.Delete "user", "CN=JSmith"

The following code snippet deletes the user object from the container under WinNT provider.

Set cont = GetObject("WinNT://ArcadiaBay")
cont.Delete "user", "jsmith"

Example Code [C++]

The following C++ code snippet shows how to delete a user using IADsContainer::Delete.

HRESULT hr;
IADsContainer *pCont=NULL;

CoInitialize(NULL);

hr = ADsGetObject(L"WinNT://myMachine",
                  IID_IADsContainer,
                  (void**) &pCont);
if ( !SUCCEEDED(hr) )
{
     return hr;
}

hr = pCont->Delete(L"user", L"JaneSmith");
pCont->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsContainer::Create


IADsContainer::get__NewEnum

The IADsContainer::get__NewEnum method creates an enumerator object that implements IEnumVARIANT to enumerate through the children of the container object.

HRESULT IADsContainer::get__NewEnum(
  IUnknown ** ppEnumerator
);

Parameters

ppEnumerator
[out] Indirect pointer to the IUnknown interface on the enumerator object for this container.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Remarks

There are two underscore characters (__) in the function name between get and NewEnum.

In Visual Basic, you can use the For…Each… statement to invoke the IADsContainer::get__NewEnum method, implicitly.

In C/C++, you can use the ADsBuildEnumerator, ADsEnumerateNext, and AdsFreeEnumerator helper functions.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to enumerate child objects in a container.

Set cont = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
For Each obj In cont
  Debug.Print obj.Name
Next

Example Code [C++]

The following C++ code snippet shows how to enumerate the object contained in a container.

IEnumVARIANT *pEnum;
IADsContainer *pCont;
LPUNKNOWN     pUnk;
VARIANT       var;
IDispatch    *pDisp;
BSTR          bstr;
ulong         lFetch;
IADs         *pADs;

// In this sample, skip error checking
ADsGetObject(L"LDAP://OU=Sales,DC=ArcadiaBay,DC=COM",
                        IID_IADsContainer, (void**) &pCont);
pCont->get__NewEnum(&pUnk);
pCont->Release();

pUnk->QueryInterface(IID_IEnumVARIANT, (void**) &pEnum);
pUnk->Release();

// Now Enumerate
HRESULT hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADs, (void**)&pADs);
        pDisp->Release();
        pADs->get_Name(&bstr);
        pADs->Release();
    }
    VariantClear(&var);
    hr = pEnum->Next(1, &var, &lFetch);
};
SysFreeString(bstr);

pEnum->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IEnumVARIANT


IADsContainer::GetObject

The IADsContainer::GetObject method obtains an IADs interface pointer to a directory object in the container. The object is identified by its schema class and the relative name.

HRESULT IADsContainer::GetObject(
  BSTR bstrClass,
  BSTR bstrRelativeName,
  IDispatch ** ppNamedObject
);

If the schema class name is not provided, retrieve an interface pointer on the first object found in the container with the specified name.

Parameters

bstrClass
[in] Name of the object as known in the underlying directory and identical to the one retrieved through the IADs::get_Class method. If the class name is NULL, the provider returns the first item found in the container.
bstrRelativeName
[in] Name of the object as known in the underlying directory and identical to the one retrieved through the IADs::get_Name method.
ppNamedObject
[out] Indirect pointer to the IDispatch interface on the specified object.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Remarks

The GetObject method is the most common way of retrieving an IADs interface pointer on an ADSI object.

Example Code [Visual Basic]

The following Visual Basic code snippet gets a user object from a container object.

Dim cont As IADsContainer
Dim usr As IADsUser
Set cont = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
Set usr = cont.GetObject("user", "jsmith")

This is equivalent to the more succinct and more efficient code:

Dim usr As IADsUser
Set usr=GetObject("LDAP://CN=jsmith, OU=Sales,DC=ArcadiaBay,DC=com")

Example Code [C++]

The following C++ code snippet gets a user object from a container object.

HRESULT hr;
CoInitialize(NULL);

IADsContainer *pCont=NULL;

hr = ADsGetObject(L"LDAP://DC=windows2000,DC=nttest,DC=microsoft,DC=com",
            IID_IADsContainer,
            (void**) &pCont );

//////////////////////////////////////////////////////////////////////
// Get the child from the container.
// Note in the LDAP provider you can go down more than one level.
//////////////////////////////////////////////////////////////////////
IDispatch *pDisp = NULL;
IADs *pADs = NULL;
hr = pCont->GetObject(L"user", L"CN=Mike Smith, OU=DSys", &pDisp );
pCont->Release();

hr = pDisp->QueryInterface( IID_IADs, (void**) &pADs );
pDisp->Release();

// Do something with pADs here.
pADs->Release();

CoUninitialize();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

ADsGetObject


IADsContainer::MoveHere

The IADsContainer::MoveHere method moves the specified object to the container implementing this interface. Objects can be moved only within the same directory system in which the container resides. The method can also be used for renaming an object.

HRESULT IADsContainer::MoveHere(
  BSTR bstrSourceObject,
  BSTR bstrNewName,
  IDispatch ** ppbstrNewObject
);

Parameters

bstrSourceObject
[in] The string specifying the ADsPath of the object to be moved.
bstrNewName
[in] A relative name the new object within the container. This can be NULL, in which case the object is just moved. If it is not NULL, the object is renamed accordingly in the process.
ppbstrNewObject
[out] Indirect pointer to the IDispatch interface on the moved object.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Remarks

The destination container must be in the same directory service as the source container. Moving objects across directory service implementations is not permitted.

The IADsContainer::MoveHere method can be used either to rename an object within the same container or to move an object among different containers. Moving an object retains the object's RDN, whereas renaming an object alters the RDN.

For example, the following code snippet does the renaming:

set cont = GetObject("LDAP://dc=dom,dc=com")

set newobj = cont.MoveHere("LDAP://cn=John Doe,dc=dom,dc=com", "cn=John Smith")

But the following code snippet does the moving:

set cont = GetObject("LDAP://dc=dom,dc=com")

set newobj = cont.MoveHere("LDAP://cn=jsmith,ou=sale,dc=dom,dc=com", "cn=jsmith")

In Visual Basic applications, you can pass vbNullString as the second parameter when moving an object from one container to another.

Set newobj = cont.MoveHere("LDAP://cn=jsmith,ou=sale,dc=dom,dc=com", vbNullString)

However, you cannot do the same with VBScript. This is because VBScript maps vbNullString to an empty string instead of to a null string, as does Visual Basic. You must use the RDN explicitly, as shown in the previous example.

Example Code [Visual Basic]

The following code snippet shows how to use this method to rename an object.

'------- Renaming an object ------------------
Set cont = GetObject("LDAP://OU=Sales, DC=ArcadiaBay,DC=com")
Set usr = cont.MoveHere("LDAP://CN=jsmith,OU=Sales, DC=ArcadiaBay,DC=com", "CN=jjohnson")

'------- Moving an object ----------------------
cont.MoveHere("LDAP://CN=chrisj,OU=Engineer,DC=ArcadiaBay,DC=com", vbNullString)

Example Code [C++]

The following C++ code snippet moves a user object using the IADsContainer::MoveHere method.

///////////////////////////////////////////////
// First, bind to the destination container.
///////////////////////////////////////////////
HRESULT hr;
IADsContainer *pCont=NULL;
CoInitialize(NULL);
hr = ADsGetObject(
        L"LDAP://OU=MCS,DC=windows2000,DC=nttest,DC=microsoft,DC=com",
        IID_IADsContainer,
        (void**) &pCont );

if ( !SUCCEEDED(hr) )
{
    return hr;
}

///////////////////////////////////////////////////////
// Now, move the object to the bound container.
///////////////////////////////////////////////////////
IDispatch *pDisp=NULL;

hr = pCont->MoveHere(L"LDAP://CN=Mike Smith,OU=DSys,DC=windows2000,DC=nttest,DC=microsoft,DC=com", NULL, &pDisp );
pCont->Release();

if (SUCCEEDED(hr) )
{
// You may do another operation here, such as updating attributes.
pDisp->Release();
}

CoUninitialize();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsContainer::CopyHere


IADsNamespaces

The IADsNamespaces interface is implemented in a namespaces object and used for managing namespace objects. A namespace object is a provider-specific top-level container and corresponds to the root node of a directory tree. The ADSI namespaces object serves as an entry point into the underlying directory and allows directory service administrators to enumerate the currently installed namespace objects.

This interface supports two property methods to get and set the DefaultContainer property which holds the path to a container object. The default container is the base node from which browsing of the directory tree proceeds. References of any children objects can be made relative to this default container. The DefaultContainer property makes it more efficient and convenient for a client to reference repetitively a contained object.

You obtain a pointer to the IADsNamespaces interface when you bind to the object using the "ADs:" string:

Dim ns As IADsNamespaces
Set ns = GetObject("ADs:")

Non-Automation clients can use the ADsGetObject helper function instead.

IADsNamespaces *pNs;
hr = ADsGetObject(L"ADs:", IID_IADsNamespaces, (void**)&pNs);

In addition to the IADsNamespaces interface, the ADSI namespaces object also implements the IADsContainer interface.

The IADsNamespaces object exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments the reference count.
Release | Decrements the reference count.

IADsNamespaces property methods | Description
get/put_DefaultContainer | Gets and sets the default container name for the current user.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsNamespaces Property Methods

The property methods of the IADsNamespaces interface get and set the properties described in the following table. For general discussions of property methods, see Interface Property Methods.

Properties in Vtable Order

Property Description
DefaultContainer

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Default
([out] BSTR *pbstrDefault);

HRESULT put_Default
([in] BSTR bstrDefault);

 The DefaultContainer property is an ADsPath name that identifies a base container object to which you can bind. This base container provides the starting point from which user begins browsing. ADSI defines the DefaultContainer property to provide a quick way of getting a pointer to a previously defined ADSI container object. Providers need to supply this property on a "per-user" basis. The default container is set immediately after the invocation of IADsNamespaces::put_DefaultContainer. SetInfo does not have to be called. In fact, the system-supplied namespaces object returns E_NOTIMPL for the SetInfo method called on this object.

Remarks

When a container is the namespaces object, an enumeration operation always results in a list of provider-specific namespace objects. When IADsContainer::GetObject is used to obtain a namespace object, the bstrClass parameter will be ignored. This is because the container, that is, the namespaces object, contains only one type of objects, namely, provider-specific namespace objects.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to set the default container of the namespaces object .

Dim ns as IADsNamespaces
Set ns = GetObject("ADs:")
ns.DefaultContainer = "WinNT://AcadiaBay/Sales"
'do other stuff

Example Code [C++]

The following C++ code snippet shows how to set the default container of the namespaces object.

IADsNamespaces *pNs;
HRESULT hr;
hr = ADsGetObject(L"ADs:",
                  IID_IADsNamespaces,
                  (void**)pNs);
VARIANT var;
VariantInit(&var);
V_BSTR(&var) = SysAllocString(L"WinNT://AcadiaBay/Sales");
V_VT(&var) = VT_BSTR;
pNs->put_DefaultContainer(var);
VariantClear(&var);

// do other stuff


IADsOpenDSObject

The IADsOpenDSObject interface is designed to supply a security context for binding to an object in the underlying directory store. It provides a means for specifying credentials of a client. Use this interface to bind to an ADSI object when you must supply a set of credentials for authentication in any directory service.

ADSI maintains the security context in its cache. Thus, throughout the connection within a process, Once authenticated, the supplied user credentials are applied to any actions performed on this object and its children. This credential caching model applies to binding to different objects as well, provided that the binding takes place within the same connection and process.

Calling the OpenDSObject method of this interface yields the cache handle. Releasing this cache handle releases the security context as well.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsOpenDSObject method | Description
OpenDSObject | Gets the IDispatch interface on the specified object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 SP2 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsClass


IADsOpenDSObject::OpenDSObject

The IADsOpenDSObject::OpenDSObject method binds to an ADSI object using the given credentials, retrieving an IDispatch pointer on the specified object.

HRESULT IADsOpenDSObject::OpenDSObject(
  BSTR bstrDNName,
  BSTR bstrUserName,
  BSTR bstrPassword,
  LONG lnReserved,
  IDispatch ** ppADSIObj
);

Parameters

bstrDNName
[in] ADsPath of the ADSI object.
bstrUserName
[in] The user name to be used for securing permission from the namespace server.
bstrPassword
[in] The password to be used for securing permission from the namespace server.
lnReserved
[in] Authentication flags used to define the binding options. For more information, see ADS_AUTHENTICATION_ENUM.
ppADSIObj
[out] Indirect pointer to an IDispatch interface on the requested object.

Return Values

This method supports the standard return values, including S_OK when the IDispatch interface has been successfully retrieved using these credentials.

For other return values, see ADSI Error Codes.

Remarks

When lnReserved is set, the behavior of OpenDSObject depends on the provider it connects to. High security namespaces may ignore these flags and always require authentication.

The IADsOpenDSObject::OpenDSObject method maintains the authenticated and encrypted user credentials in the cache. Such cached credentials may be used in subsequent operations for binding to any other directory objects. The ADSI client applications should not cache the credentials supplied by the user. Instead, they should rely on ADSI infrastructure to do the caching.

To use the cached credentials, set the bstrPassword parameter to null and leave the bstrUserName unchanged in any subsequent calls of OpenDSObject. The following code segment illustrates this point.

Dim dso As IADsOpenDSObject
Dim obj1, obj2 As IADs

Set dso = GetObject("LDAP:")

' supply full credentials to initiate a connection to a server.
Set obj1 = dso.OpenDSObject( _
                "LDAP://server1/CN=Dept1,DC=ArcadiaBay,DC=com", _
                "JSmith", _
                "secrete", _
                ADS_SECURE_AUTHENTICATION)

' do something with the bound object, obj1
MsgBox obj1.Class

' bind to another object with cached user credential
Set obj2 = dso.OpenDSObject( _
                "LDAP://server1/CN=Dept2,DC=ArcadiaBay,DC=com", _
                "JSmith", _
                 vbNullString, _
                 ADS_SECURE_AUTHENTICATION)

MsgBox obj2.Class

With serverless bindings, the server name, server1, is not stated explicitly. The default server is used instead.

In order for credential caching to work properly, it is important to keep an object reference outstanding to maintain the cache handle. In the example given above, an attempt to open obj2 after releasing obj1 will result in an authentication failure.

The IADsOpenDSObject method uses the default credentials when bstrUserName and bstrPassword are set to NULL.

The ADsOpenObject helper function offers the same functionality as the IADsOpenDSObject::OpenDSObject method.

With the WinNT provider, you may pass in bstrUserName as one of the following strings:

  • The name of a user account, such as jsmith.
  • The Windows NT® 4.0 style user name, such as ArcadiaBay\jsmith.

With the LDAP provider for Active Directory, you may pass in bstrUserName as one of the following strings:

  • The name of a user account, such as jsmith.
  • The user path from a previous version of Windows NT, such as AcardiaBay\jsmith.
  • Distinguished Name, such as CN=jsmith, OUT=Sales, DC=ArcardiaBay, DC=Com.
  • User Principal Name (UPN), such as jsmith@Arcadiabay.com.

To use the UPN, you must set the appropriate UPN value for the userPrincipalName property of the user.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to use IADsOpenDSObject to open the "Administrator" user object on "ArcadiaBay" with Secure Authentication through the WinNT provider.

Dim dso As IADsOpenDSObject
Dim domain As IADsDomain

Set dso = GetObject("WinNT:")
Set domain = dso.OpenDSObject("WinNT://ArcadiaBay", "Administrator", "secret", ADS_SECURE_AUTHENTICATION)

Example Code [C++]

The following is a C++ code snippet that uses IADsOpenDSObject to open an Active Directory object through the LDAP provider.

IADsOpenDSObject *pDSO;
HRESULT hr;

hr = ADsGetObject(L"LDAP:", IID_IADsOpenDSObject, (void**) &pDSO);
if (SUCCEEDED(hr))
{
    IDispatch *pDisp;
    hr = pDSO->OpenDSObject(L"LDAP://DC=ArcadiaBay, DC=com",
                       L"jsmith@arcadiabay.com",L"passwordhere",
                       ADS_SECURE_AUTHENTICATION, &pDisp);
    pDSO->Release();
    if (SUCCEEDED(hr))
    {
        IADs *pADs;
        hr = pDisp->QueryInterface(IID_IADs, (void**) &pADs);
        pDisp->Release();
        if (SUCCEEDED(hr))
        {
        // do some object manipulation here.
            pADs->Release();
        }
    }
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 SP2 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsOpenDSObject


Schema Interfaces

This section describes the following schema interfaces:

  • IADsClass
  • IADsProperty
  • IADsSyntax


IADsClass

The IADsClass interface is designed for managing schema class objects that provide class definitions for any ADSI object. Other schema management interfaces include IADsProperty for attribute definitions and IADsSyntax for attribute syntax.

The IADsClass interface supports the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of the object's programmable interface.
GetIDsOfNames | Maps the name of the method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsClass properties and methods | Description
get_PrimaryInterface | Gets the identifier of the interface defining this schema class.
get/put_CLSID | Gets and sets the CLSID identifying application component that implements this schema class.
get/put_OID | Gets and sets the directory-specific object identifier.
get/put_Abstract | Gets and sets the flag in order to determine whether or not this schema class is abstract.
get/put_Auxiliary | Gets and set the flag in order to determine whether or not this schema class is auxiliary.
get/put_MandatoryProperties | Gets and sets a list of names of mandatory properties an ADSI object must have.
get/put_OptionalProperties | Gets and sets a list of names of optional properties an ADSI object may have.
get/put_NamingProperties | Gets and sets a list of naming attributes for the schema object.
get/put_DerivedFrom | Gets and sets the immediate super class of this schema class.
get/put_AuxDerivedFrom | Gets and sets the immediate super Auxiliary class of this schema class.
get/put_PossibleSuperiors | Gets and sets a list of classes that can contain instances of this class.
get/put_Containment | Gets and sets legal objects types that can be contained within this container.
get/put_Container | Gets and sets the flag to indicate whether or not this is a container object.
get/put_HelpFileName | Gets and sets the name of an optional help file.
get/put_HelpFileContext | Gets and sets the context identifier of an optional help file.
Qualifiers | Returns a collection of ADSI objects describing provider-specific qualifiers for the schema.

Remarks

A schema defines the universe of objects that can be stored in a directory. In Active Directory™, the schema specifies what attributes a directory service object can or must have. It also specifies the value range and the syntax of the attributes and whether they support single or multiple values. In short, the schema is organized by class definitions, attribute definitions and attribute syntax. In ADSI, the schema is collectively managed through the IADsClass, IADsProperty and IADsSyntax interfaces.

Active Directory uses a set of schema objects to provide dynamically extensible schema management. To find out more about an unknown object, you can look up its associated schema objects. To create a new class definition or extend an existing one, you create or extend the appropriate schema objects. Schema objects are organized in the schema container of a given directory. To access an object's schema class, you use the object's Schema property (namely, call the IADs::get_Schema property method) to obtain the ADsPath string and use that string to bind to its schema class object. Example codes are given at the end of this page.

To determine attribute definitions, that is, the range of values, the syntax, and so on, you need to inspect the schema attribute objects for each property supported by the directory service object. For discussions on how to access the schema attribute objects, see IADsProperty.

Example Code [Visual Basic]

The following Visual Basic® code snippet shows how to work with the IADsClass interface.

Dim obj As IADs
Dim cls As IADsClass
Set obj = GetObject("WinNT://myMachine,computer")
Set cls = GetObject(obj.Schema)

' inspecting mandatory and optional properties:
For Each p in cls.MandatoryProperties
    MsgBox "Must-have: " & p
Next
For Each p in cls.OptionalProperties
    MsgBox "May-have: " & p
Next

Here, p is the name of an attribute. For example, the computer object has an optional attribute known as Owner.

Example Code [C++]

The following C++ code snippet shows how to work with the IADsClass interface.

HRESULT hr;
IADsClass *pCls;
IADs *pADs;
BSTR bstrSchema, bstr;
VARIANT var;

hr = CoInitialize(NULL);
hr = ADsGetObject(L"WinNT://myComputer,computer",
                  IID_IADs,
                  (void**)&pADs);
if (FAILED(hr)) { return hr;}

hr = pADs->get_Schema(&bstrSchema);
pADs->Release();
if(FAILED(hr)) { return hr;    }

hr = ADsGetObject(bstrSchema, IID_IADsClass, (void**)&pCls);
if(FAILED(hr)) { return hr;    }

VariantInit(&var);
pCls->get_MandatoryProperties(&var);
hr = printVarArray(var);

VariantClear(&var);
pCls->get_OptionalProperties(&var);
hr = printVarArray(var);

CoUninitialize();

The printVarArray function is implemented as follows:

HRESULT printVarArray(VARIANT var)
{
    LONG lstart, lend;
    VARIANT varItem;
    HRESULT hr;
    SAFEARRAY *sa = V_ARRAY( &var );
    hr = SafeArrayGetLBound( sa, 1, &lstart );
    hr = SafeArrayGetUBound( sa, 1, &lend );
    VariantInit(&varItem);
    for ( long idx=lstart; idx < lend; idx++ ) {
        hr = SafeArrayGetElement( sa, &idx, &varItem );
        printf("   %S \n", V_BSTR(&varItem));
        VariantClear(&varItem);
    }
    printf("\n");
    return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsProperty, IADsSyntax, IADsContainer


IADsClass Property Methods

The property methods of the IADsClass interface get or set the following properties. For general discussions of property methods, see Interface Property Methods.

Properties in Vtable Order

Property Description
Abstract

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_Abstract
([out] BOOLEAN *pbAbstract);

HRESULT put_Abstract
([in] BOOLEAN bAbstract);

 Boolean value indicating whether this class is Abstract or non-abstract. When TRUE this class is an Abstract class and cannot be directly instantiated in the directory service. Abstract classes can only be used as super classes.
AuxDerivedFrom

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_AuxDerivedFrom
([out] VARIANT *pvAuxDerivedFrom);

HRESULT put_AuxDerivedFrom
([in] VARIANT vAuxDerivedFrom);

 Array of ADsPath strings that indicate the super Auxiliary classes this class derives from.
Auxiliary

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_Auxiliary
([out] BOOLEAN *pbAuxiliary);

HRESULT put_Auxiliary
([in] BOOLEAN bAuxiliary);

 Boolean value indicating whether or not this class is Auxiliary. When TRUE this class is an Auxiliary class and cannot be directly instantiated in the directory service. Auxiliary classes can only be used as super classes of other Auxiliary classes or as a source of additional properties on structural classes.
CLSID

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_CLSID
([out] BSTR *pbstrCLSID);

HRESULT put_CLSID
([in] BSTR bstrCLSID);

 Optional provider-specific CLSID identifying the COM object implementing this class.
Container

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_Container
([out] BOOLEAN *pbContainer);

HRESULT put_Container
([in] BOOLEAN bContainer);

 Boolean value indicating if this class is a directory container object.
Containment

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Containment
([out] VARIANT *pvContainment);

HRESULT put_Containment
([in] VARIANT vContainment);

 Container types that can contain objects of this class. Each container name is an element in a BSTR array.
DerivedFrom

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_DerivedFrom
([out] VARIANT *pvDerivedFrom);

HRESULT put_DerivedFrom
([in] VARIANT vDerivedFrom);

 Array of ADsPath strings that indicate which classes this class was derived from.
HelpFileContext

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HelpFileContext
([out] BSTR *pbstrHelpFileContext);

HRESULT put_HelpFileContext
([in] BSTR bstrHelpFileContext);

 Context ID inside HelpFileName where specific information on this class can be found.
HelpFileName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HelpFileName
([out] BSTR *pbstrHelpFileName);

HRESULT put_HelpFileName
([in] BSTR bstrHelpFileName);

 Name of a help file that contains further information about objects of this class.
MandatoryProperties

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_MandatoryProperties
([out] VARIANT *pvarMandatoryProperties);

HRESULT put_MandatoryProperties
([in] VARIANT varMandatoryProperties);

 SAFEARRAY of BSTRs listing the properties that must be set for this class to be written to storage.
OptionalProperties

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_OptionalProperties
([out] VARIANT *pvarOptionalProperties);

HRESULT put_OptionalProperties
([in] VARIANT varOptionalProperties);

 SAFEARRAY of BSTRs listing the properties that are optional for this schema class.
NamingProperties

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_NamingProperties
([out] VARIANT *pvarNamingProperties);

HRESULT put_NamingProperties
([in] VARIANT varNamingProperties);

 SAFEARRAY of BSTRs listing the properties that are for naming attributes of this schema class. For example, in NDS, "CN" and "OU" are naming properties.
OID

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_OID
([out] BSTR *pbstrOID);

HRESULT put_OID
([in] BSTR bstrOID);

 Provider-specific Object Identifier defining this class. This is provided to allow schema extension using Active Directory in directory services that require provider-specific OIDs for classes.
PossibleSuperiors

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_PossibleSuperiors
([out] VARIANT *pvSuperiors);

HRESULT put_PossibleSuperiors
([in] VARIANT vSuperiors);

 Array of ADsPath strings that indicate the schema classes that can contain instances of this class.
PrimaryInterface

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_PrimaryInterface
([out] BSTR *pbstrGUID);

 Optional provider-specific identifier GUID that associates an interface with objects of this schema class. For example, the "User" class supports IADsUser and PrimaryInterface is identified by IID_IADsUser. This must be in the standard string format of a GUID as defined by COM. This GUID is the value that will appear in the IADs::get_GUID property in instances of this class for providers that implement this property. Identifying a schema class by IID of the class code's primary interface enables the use of QueryInterface at run time to determine whether an object is of the desired class.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use the IADsClass interface to determine if an object is a container and, if so, lists the names of the contained objects.

Dim ads As IADs
Dim cls As IADsClass
Set ads = GetObject("WinNT://myComputer,computer")
Set cls = GetObject(ads.Schema)
if cls.Container = True Then
    MsgBox "This object contains the following children:"
    For each o in cls.Containment
        MsgBox o
    Next
End If

Example Code [C++]

The following C++ code snippet shows how to use the IADsClass interface to determine if an object is a container and, if so, lists the names of the contained objects.

HRESULT hr;
IADsClass *pCls;
IADs *pADs;
BSTR bstrSchema, bstr;
VARIANT var;

hr = CoInitialize(NULL);
hr = ADsGetObject(L"WinNT://myComputer,computer",
                  IID_IADs,
                  (void**)&pADs);
if (FAILED(hr)) { return hr;}

hr = pADs->get_Schema(&bstrSchema);
pADs->Release();
if(FAILED(hr)) { return hr;    }

hr = ADsGetObject(bstrSchema, IID_IADsClass, (void**)&pCls);
if(FAILED(hr)) { return hr;    }

VariantInit(&var);
VARIANT_BOOL bCont;
pCls->get_Container(&bCont);
if(bCont != false) {
    VariantClear(&var);
    pCls->get_Containment(&var);
    hr = printVarArray(var);
}
CoUninitialize();

See Also

IADsClass::Qualifiers


IADsClass::Qualifiers

The IADsClass::Qualifiers method is an optional method that returns a collection of ADSI objects describing additional qualifiers of this schema class.

HRESULT IADsClass::Qualifiers(
  IADsCollection ** ppQualifiers
);

Parameters

ppQualifiers
[out] Address of IADsCollection* pointer variable that receives the interface pointer to the ADSI collection object representing additional limits for this schema class.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The IADsCollection interface pointer has been successfully retrieved.
E_FAIL
The operation has failed.
E_NOTIMPL
The method is not supported by the given provider.

For other return values, see ADSI Error Codes.

Remarks

The qualifier objects are provider-specific. When supported, this method can be used to obtain extended schema information.

This method is not currently supported by any of the providers supplied by Microsoft®.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to use this method.

Dim ads As IADs
Dim cls As IADsClass
Set ads = GetObject("WinNT://myComputer, computer")
Set cls = GetObject(ads.Schema)

' show the user where to look for additional class info.
ListBox.additem "Additional class info can be found from:"
For each q in cls.Qualifiers
    listBox.additem q.Name
Next

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsProperty::Qualifiers


IADsProperty

The IADsProperty interface is designed to manage a single attribute definition for a schema class object. An attribute definition specifies the minimum and maximum values of a property, its syntax, and whether the property supports multiple values. Other interfaces involved in schema management include IADsClass and IADsSyntax.

IADsProperty exposes methods to describe a property by name, syntax, value ranges, and any other attributes that have been defined. A property can have multiple names associated with it. But providers are required to ensure that all names are unique.

You can use the IADsProperty interface to determine at run-time the attribute definition of a property supported by a directory service object. The following procedure describes how this can be done.

  1. Bind to the ADSI object's schema class object.
  2. Enumerate mandatory or optional attributes accessible from the schema class object. Skip this step if you know that the object supports the attribute of your interest.
  3. Bind to the schema container of the schema class object as obtained in Step 1.
  4. Retrieve from the schema container the attribute definition object of the property of interest.
  5. Examine the attribute definition of the property. This may require you to inspect the syntax object as well.

Example Code [Visual Basic]

The following code example illustrates the procedure above for applying the IADsProperty interface to determine attribute definitions of a property.

Dim obj As IADs
Dim cl As IADsClass
Dim pr As IADsProperty
Dim sy As IADsSyntax
Dim sc As IADsContainer

' Step 1
Set obj = GetObject("WinNT://myMachine,computer")
Set cl = GetObject(obj.Schema)

' Step 2
' Skip it, assuming the "Owner" attribute is supported by obj.
' For the computer object in this example, it is indeed one of
' the supported optional properties.

' step 3
Set sc = GetObject(cl.Parent)

' Step 4
Set pr = sc.GetObject("Property","Owner")

' Step 5
MsgBox "Attribute: " & pr.Name
MsgBox "Syntax:    " & pr.Syntax
If pr.Multivalued = True Then
    MsgBox "The Owner attribute has multiple values."
Else
    MsgBox "The Owner attribute has a single value."
End If

' to further examine the syntax
Set sy = GetObject(sc.AdsPath & "/" & pr.Syntax)
MsgBox "Syntax object: " & sy.Name & " of OleAutoDataType: " & sy.OleAutoDataType

The IADsProperty interface's methods can also be used to add new attributes and property objects to a provider-specific implementation.

The IADsProperty interface supports the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsProperty property methods | Description
get/put_OID | Directory-specific object identifier.
get/put_Syntax | Relative path of syntax object.
get/put_MaxRange | Upper limit of values.
get/put_MinRange | Lower limit of values.
get/put_MultiValued | Whether or not this is a property that supports multiple values.
Qualifiers | Optional additional provider-specific constraints on this property.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsProperty Property Methods

The property methods of the IADsProperty interface read and write the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
OID

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_OID
([out] BSTR *bstrOID);

HRESULT put_OID
([in ]BSTR bstrOID);

 Directory-specific object identifier.
Syntax

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Syntax
([out] BSTR *bstrSyntax);

HRESULT put_Syntax
([in] BSTR bstrSyntax);

 Relative path of syntax object.
MaxRange

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]

HRESULT get_MaxRange
([out] LONG *lnMaxRange);

HRESULT put_MaxRange
([in] LONG lnMaxRange);

 Upper limit of values.
MinRange

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MinRange
([out] LONG *lnMinRange);

HRESULT put_MinRange
([in] LONG lnMinRange);

 Lower limit of values.
MultiValued

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_MultiValues
([out] VARIANT *fMultivalued);

HRESULT put_MultiValues
([in] VARIANT fMultivalued);

 Whether property supports single or multiple values.

Example Code [Visual Basic]

The following code snippet examines the OperatingSystem attribute of a computer on a network through the WinNT provider.

Dim obj As IADs
Dim cl As IADsClass
Dim pr As IADsProperty
Dim sc As IADsContainer

Set obj = GetObject("WinNT://myMachine,computer")
Set cl = GetObject(obj.Schema)
Set sc = GetObject(cl.Parent)
Set pr = sc.GetObject("Property","OperatingSystem")

MsgBox "Attribute:  " & pr.Name
MsgBox "Syntax:     " & pr.Syntax
MsgBox "MaxRange:   " & pr.MaxRange
MsgBox "MinRange:   " & pr.MinRange
MsgBox "Multivalued:" & pr.Multivalued

Example Code [C++]

The following code snippet examines the OperatingSystem attribute of a computer on a network through the WinNT provider. For brevity, error checking is omitted.

IADs *pADs;
IADsClass *pCls;
IADsProperty* pProp;
IADsContainer *pCont;
long lval;
short bval;
HRESULT hr = CoInitialize(NULL);

hr = ADsGetObject(L"WinNT://myMachine,computer",
                  IID_IADs, (void**)&pADs);
BSTR bstr;
pADs->get_Schema(&bstr);
pADs->Release();
hr = ADsGetObject(bstr, IID_IADsClass, (void**)&pCls);
pCls->get_Parent(&bstr);
pCls->Release();
hr = ADsGetObject(bstr, IID_IADsContainer, (void**)&pCont);
pCont->GetObject(L"Property", L"OperatingSystem",(void**)&pProp);
pCont->Release();
pProp->get_Name(&bstr);
printf(" Name : %S\n",bstr);
pProp->get_Syntax(&bstr);
printf(" Syntax : %S\n,bstr);
pProp->get_MaxRange(&lval);
printf(" MaxRange : %d\n",lval);
pProp->get_MinRange(&lval);
printf(" MinRange : %d\n", lval);
pProp->get_Multivalued(&bval);
printf(" Multivalued : %b\n",bval);

CoUninitialize();

See Also

IADsClass, IADsSyntax


IADsProperty::Qualifiers

The IADsProperty::Qualifiers method is an optional method that returns a collection of ADSI objects describing additional qualifiers of this property.

HRESULT IADsProperty::Qualifiers(
  IADsCollection ** ppQualifiers
);

When to Implement

Return E_NOTIMPL if you do not support this interface.

Parameters

ppQualifiers
[out] Indirect pointer to the IADsCollection interface on the ADSI collection object that represents additional limits for this property.

Return Values

This method supports the standard return values E_FAIL and E_UNEXPECTED, as well as the following:

S_OK
The IADsCollection interface pointer has been successfully retrieved.
For other return values, see ADSI Error Codes.

Remarks

ADSI calls this method when it detects any provider-specific constraints on the uses or values of this property.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsClass::Qualifiers


IADsSyntax

The IADsSyntax interface specifies methods to identify and modify the available Automation Data Types for representing its data. ADSI defines a standard set of syntax objects that can be used uniformly across multiple directory service implementations. Other interfaces for schema management include IADsClass, IADsProperty.

Use the IADsSyntax interface to process the property values of any instance of ADSI schema class object.

The IADsSyntax interface supports the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsSyntax property methods | Description
get/put_OleAutoDataType | The Automation data type constant, VT_xxxx.

Inheritance: Inherited from IADs, IDispatch, IUnknown

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsSyntax Property Methods

The property methods of the IADsSyntax interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
OleAutoDataType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_OleAutoDataType
([out] LONG *plAutoDataType);

HRESULT put_OleAutoDataType
([in] LONG lAutoDataType);

 Gets and sets a long containing the value of the VT_xxx constant for the Automation Data Type representing this syntax.

Active Directory supports the following Automation Data Types:
Virtual Types
VT_BOOL
VT_BSTR
VT_BSTRT
VT_CY
VT_DATE
VT_EMPTY
VT_ERROR
VT_I2
VT_I4
VT_R4
VT_R8
VT_UI1 		Description
BOOL
BSTR
BSTRT
CURRENCY
Date
NULL
Invalid
short
long
real
double
BYTE

Remarks

An array of unsigned bytes, VT_UI1|VT_ARRAY could mean that the provider has mapped a syntax that cannot be mapped to an Automation Virtual Type.

Example Code [Visual Basic]

The following Visual Basic code snippet examines the syntax of the OperatingSystemVersion attribute of a computer on a network through the WinNT provider. The resultant syntax is a string.

Dim obj As IADs
Dim cl As IADsClass
Dim pr As IADsProperty
Dim sy As IADsSyntax
Dim sc As IADsContainer

Set obj = GetObject("WinNT://myMachine,computer")
Set cl = GetObject(obj.Schema)
Set sc = GetObject(cl.Parent)
Set pr = sc.GetObject("Property","OperatingSystemVersion")
Set sy = GetObject(sc.ADsPath & "/" & pr.Syntax)

MsgBox "Automation data types: " & sy.OleAutoDataType

Example Code [C++]

The following C++ code snippet examines the syntax of the OperatingSystemVersion attribute of a computer on a network through the WinNT provider. The resultant syntax is a string. For brevity, error checking is omitted.

#include <stdio.h>
#include <activeds.h>

IADs *pObj;
IADsClass *pCls;
IADsProperty *pProp;
IADsSyntax *pSyn;
IADsContainer *pCont;

HRESULT hr = ADsGetObject(L"WinNT://myMachine,computer",
                          IID_IADs,
                          (void**)&pObj);
VARIANT var;
VariantInit(&var);
BSTR bstr;

pObj->get_Schema(&bstr);
pObj->Release();
printf("Object schema: %S\n",bstr);

hr = ADsGetObject(bstr, IID_IADsClass,(void**)&pCls);
pCls->get_Parent(&bstr);
pCls->Release();
printf("Object class's container:          %S\n", bstr);

hr = ADsGetObject(bstr, IID_IADsContainer, (void**)&pCont);
pCont->QueryInterface(IID_IADs,(void**)&pObj);
pCont->Release();
pObj->get_ADsPath(&bstr);
printf("               container's AdsPath: %S\n",bstr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Property",
                      L"OperatingSystemVersion",
                      (IDispatch**)&pDisp);
pCont->Release();

pDisp->QueryInterface(IID_IADsProperty, (void**)&pProp);
pDisp->Release();

BSTR bstr1;
pProp->get_Syntax(&bstr1);
pProp->Release();
printf("Property Syntax:                   %S\n",bstr1);

wcscat(bstr, L"/");
wcscat(bstr, bstr1);
printf("ADsPath of the syntax object       %S\n",bstr);

hr = ADsGetObject(bstr, IID_IADsSyntax, (void**)&pSyn);
long lType;
pSyn->get_OleAutoDataType(&lType);
printf("Property syntax's OleAutoDataType: %d\n", lType);
pSyn->Release();

SysFreeString(bstr);
SysFreeString(bstr1);

See Also

IADsClass, IADsProperty


Property Cache Interfaces

The property cache interfaces are designed to work with properties in cache. They consist of the following interfaces:

  • IADsPropertyEntry
  • IADsPropertyList
  • IADsPropertyValue
  • IADsPropertyValue2


IADsPropertyEntry

The IADsPropertyEntry interface is used to manage a property entry in the property cache. A property entry holds a value (or values) of an attribute as defined in the schema. It is identified by the name of the corresponding attribute. A property entry object allows a user to specify how its values are to be manipulated. Examples of such operations include "update", "modify", and "delete".

Multiple property entries are managed by a property list. To access a property entry, you use the Item property or call the GetPropertyItem method on the IADsPropertyList interface.

Use the property methods of IADsPropertyEntry to examine and manipulate individual properties. Before calling the methods of this interface, you must call IADs::GetInfo or IADs::GetInfoEx explicitly to load the assigned property values of the object into the cache. After calling the methods of this interfaces, you must call IADs::SetInfo to save the changes in the persistent store of the underlying directory.

The IADsPropertyEntry interface inherits from the IDispatch interface and exposes several property methods, as summarized in the following table.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPropertyEntry property methods | Description
get/put_Name | Gets and sets the name of the property entry.
get/put_ADS_Type | Gets and sets the ADS data type of the property entry.
get/put_ControlCode | Gets and sets
get/put_Values | Gets and sets the current values of a property entry.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPropertyList, IADsPropertyValue, Property Cache


IADsPropertyEntry Property Methods

The property methods of the IADsPropertyEntry interface provide access to the following properties. For a general discussion on property methods, see Interface Property Methods.

Properties in Vtable Order

Property Description
Name

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Name
([out] BSTR *pbstrName);

HRESULT put_Name
([in] BSTR bstrName);

 Name of the property entry. This name should correspond to the name of an attribute as defined in the schema.
ADsType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ADsType
([out] LONG *plADsType);

HRESULT put_ADsType
([in] LONG lADsType);

 The data type of the Name property. The values of the data type is defined in the ADSTYPE enumeration.
ControlCode

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ControlCode
([out] LONG *pControlCode);

HRESULT put_ControlCode
([in] LONG lnControlCode);

 A constant that specify the operation to be performed on the named property. The value is defined in the ADS_PROPERTY_OPERATION_ENUM enumeration.
Values

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Values
([out] VARIANT *pvValues);

HRESULT put_Values
([in] VARIANT vValues);

 A VARIANT array. Each element in this array represents a value of the named property. Such property values are represented by ADSI objects implementing the IADsPropertyValue interfaces. Therefore, the VARIANT array holds an array of pointers to the IDispatch interface on the ADSI objects implementing the IADsPropertyValue interface.

Return Values

Each property method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Code Example [Visual Basic]

The following Visual Basic® code snippet shows how to retrieve a named property from the cache and create a new property entry.

'------------------------------------------------------------
'  --- Getting IADsPropertyEntry ----------------------------
'------------------------------------------------------------

' create the property list object
Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

' get a named property entry object.
Set propEntry = propList.GetPropertyItem("dc", ADSTYPE_CASE_IGNORE_STRING)

'------------------------------------------------------------
'--- Setting IADsPropertyEntry ------------------------------
'------------------------------------------------------------

' create a property value object
Set propVal = New PropertyValue

'--- Property Value-----
propVal.CaseIgnoreString = "Arcadia Bay, Inc - Seattle, WA"
propVal.ADsType = ADSTYPE_CASE_IGNORE_STRING

'—Make a new Property Entry ----
Set propEntry = New PropertyEntry
propEntry.Name = "adminDescription"
propEntry.Values = Array(propVal)
propEntry.ControlCode = ADS_PROPERTY_UPDATE
propEntry.ADsType = ADS_CASE_IGNORE_STRING

' --- Put the newly created property entry to the cache----
propList.PutPropertyItem (propEntry)

' commit the entry to the directory store.
propList.SetInfo

Code Example [C++]

The following C++ code snippet shows how to retrieve a named property from a cache:

#include <activeds.h>
#include <stdio.h>

IADsPropertyList *pList;
IADsPropertyEntry *pEntry;
IADs *pObj;
VARIANT var;
long valType = ADSTYPE_CASE_IGNORE_STRING;

VariantInit(&var);

// bind to directory object
HRESULT hr = ADsGetObject(L"LDAP://dc01/DC=ArcadiaBay,DC=com",
                          IID_IADsPropertyList,
                          (void**)&pList);

// initialize the property cache
hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
pObj->GetInfo();
pObj->Release();

// get a property entry
hr = pList->GetPropertyItem(L"description", valType, &var);
pList->Release();
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                      (void**)&pEntry);
VariantClear(&var);

// get the name and the type of the property entry
BSTR nm;
hr = pEntry->get_Name(&nm);
printf("Property name = %S\n",nm);
VariantClear(&var);
long at;
hr = pEntry->get_ADsType(&at);
printf("Property type = %d\n",a);

See Also

ADSTYPE, Using Directory Service Queries, IADsPropertyList, IADsPropertyValue, Property Cache


IADsPropertyList

The IADsPropertyList interface is used to modify, read, and update a list of property entries in the property cache of an object. It serves to enumerate, modify and purge the contained property entries. You can use the enumeration method of this interface to find out all the initialized properties. This is different from using the schema to determine all possible attributes an ADSI object can have and which properties have been set.

Call the methods of the IADsPropertyList interface to examine and manipulate the property list on the client. Before calling the methods of this interface, you must call IADs::GetInfo or IADs::GetInfoEx explicitly to load the assigned property values of the object into the cache. After calling the methods of this interface, you must call IADs::SetInfo to save the changes in the persistent store of the underlying directory.

To obtain the property list of an ADSI object, you bind to its IADsPropertyList interface. You must call the GetInfo method before calling other methods of property list object, if the property cache has not been initialized. For examples, see the Example Code.

The IADsPropertyList interface inherits from IDispatch and exposes the following methods and property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPropertyList property methods | Description
get_PropertyCount | Gets the number of properties in the property list.
Next | Gets the next item in the property list.
Skip | Skips a specified number of items in the property list.
Reset | Moves back to the start of the list.
Item | Gets a property that is specified by name or by index.
GetPropertyItem | Gets the value of a named property.
PutPropertyItem | Puts the value of a named property.
ResetPropertyItem | Resets the value of a named property.
PurgePropertyList | Deletes all properties from the list and, therefore, releases the property caches as well.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to bind to the IADsPropertyList interface and then initialize the property cache.

IADsPropertyList pList
Set pList = GetObject("LDAP://dc01/dc=ArcadiaBay,dc=com")
pList.GetInfo

Example Code [C++]

The following C++ code snippet shows how to bind to the IADsPropertyList interface and initialize the property cache.

IADsPropertyList* GetPropertyCache(LPWSTR adsPath)
{
    IADsPropertyList *pList;
    IADs *pObj;
    HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsPropertyList,
                          (void**)&pList);
    // initialize the property cache
    hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
    pObj->GetInfo();
    pObj->Release();

    // get the property list.
    hr = pObj->QueryInterface(IID_IADsPropertyList,(void**)&pList);
    pObj->Release();

    return pList;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList Property Methods

The property methods of the IADsPropertyList interface read the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
PropertyCount

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_PropertyCount
([out] LONG *plCount);

 The number of items in the property list.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to determine number of items in a property list.

Dim propList As IADsPropertyList
Dim count As Long

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")

propList.GetInfo
count = propList.PropertyCount
Debug.Print "Number of Properties Found: " & count

Example Code [C++]

The following C++ code snippet shows how to determine number of items in a property list. The listing of the GetPropertyCache(LPWSTR) function is given in IADsPropertyList.

IADsPropertyList* GetPropertyCache(LPWSTR);

LPWSTR adsPath = L"LDAP://dc01/DC=ArcadiaBay,DC=com";
IADsPropertyList *pList=GetPropertyCache(adsPath);

long count;
hr = pList->get_PropertyCount(&count);
printf(" Number of properties = %d\n", count);

See Also

IADsPropertyEntry, IADsPropertyValue, Property Cache


IADsPropertyList::GetPropertyItem

The IADsPropertyList::GetPropertyItem method retrieves the item that matches the name from the list.

HRESULT GetPropertyItem(
  BSTR bstrName,
  LONG lnADsType,
  VARIANT * pVariant
);

Parameters

bstrName
[in] Name of the requested property.
lnADsType
[in] Type to be used in interpreting the requested property. Values are taken from the ADSTYPE enumeration. If the type is not known, this parameter can be set to ADSTYPE_UNKNOWN. For schema-less servers, the user must specify the type.
pVariant
[out] Address of a method-allocated variant containing the requested property item. If this parameter is NULL on return, the requested item could not be obtained.

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

As a caller, you are responsible for freeing the VARIANT variable allocated by GetPropertyItem.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to retrieve a property entry using the GetPropertyItem method.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim propVal As IADsPropertyValue

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

Set propEntry = propList.GetPropertyItem("dc", ADSTYPE_CASE_IGNORE_STRING)

For Each v In propEntry.Values
    Set propVal = v
    Debug.Print propVal.CaseIgnoreString
Next

Example Code [C++]

The following C++ code snippet shows how to retrieve a property entry using the GetPropertyItem method. It assumes that the IADsPropertyList interface has been properly obtained. For more information on how to load the property cache, see the code listing of the GetPropertyCache(LPWSTR) function given in IADsPropertyList.

#include <activeds.h>
#include <stdio.h>

/////////////////////////////////////////////////////////
// Function to retrieve a specified property entry
// using the IADsPropertyList::GetPropertyItem method.
/////////////////////////////////////////////////////////
IADsPropertyEntry *GetPropertyItem(
      IADsPropertyList *pList,
      LPWSTR entryName,
      long entryType)
{
   IADsPropertyEntry *pEntry;
   VARIANT var;
   VariantInit(&var);

   // get a property entry
   hr = pList->GetPropertyItem(entryName, entryType, &var);
   hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                         (void**)&pEntry);
   VariantClear(&var);

   return pEntry;
}

///////////////////////////////////////////////////////
// Examine a property entry.
///////////////////////////////////////////////////////
IADsPropertyList *pList;
pList = GetPropertyCache(L"LDAP://dc01/DC=ArcadiaBay,DC=COM");

IADsPropertyEntry *pEntry;
pEntry = GetPropertyItem(pList, L"dc", ADSTYPE_CASE_IGNORE_STRING)

BSTR nm;
HRESULT hr = pEntry->get_Name(&nm);
printf("Property name = %S\n",nm);

if(pList) pList->Release();
if(pEntry) pEntry->Release();
if(nm) SysFreeString(&nm);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::Item

The IADsPropertyList::Item method retrieves the specified property item from the list.

HRESULT Item(
  VARIANT varIndex,
  VARIANT * pVariant
);

Parameters

varIndex
[in] VARIANT containing the index or name of the property to be retrieved.
pVariant
[out] Address of a method-allocated VARIANT variable containing the value of the property.

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

As a caller, you are responsible for freeing the Variant variable allocated by Item. The following Visual Basic code snippet shows how to enumerate all the entries with the Item method.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim count As Long

Set propList = GetObject("LDAP://dc02/ DC=ArcadiaBay,DC=com")

propList.GetInfo
count = propList.PropertyCount
Debug.Print "No of Property Found: " & count

'==== Getting the property list item with Name ==================
Set propEntry = propList.Item("uSNCreated")
Debug.Print propEntry.Name
Debug.Print propEntry.ADsType

' to examine property entries by name and type
For i = 0 To count - 1
    '==== Getting the property list item with Number =============
    Set propEntry = propList.Item(i)
    Debug.Print propEntry.Name
    Debug.Print propEntry.ADsType
Next

Example Code [C++]

The following C++ code snippet shows how to retrieve the Owner property of a computer using the IADsPropertyList::Item method.

////////////////////////////////////////
// function:    PropertyItem
//    input:    PropertyList,
//              name of the item
//   output:    Property entry
//     uses:    IADsPropertyList::Item
////////////////////////////////////////
IADsPropertyEntry *PropertyItem(
         IADsPropertyList *pList,
         LPWSTR item)
{
    IADsPropertyEntry *pEntry;
    VARIANT varEntry, varItem;
    VariantInit(&varItem);
    VariantInit(&varEntry);

    // get a property entry
    V_BSTR(&varItem)= SysAllocString(item);
    V_VT(&varItem)=VT_BSTR;
    HRESULT hr = pList->Item(varItem ,&varEntry);
    hr = V_DISPATCH(&var)->QueryInterface(
                        IID_IADsPropertyEntry,
                        (void**)&pEntry);
    VariantClear(&varItem);
    VariantClear(&varEntry);
    return pEntry;
}

///////////////////////////////////////
// examine a property entry
///////////////////////////////////////
IADsPropertyList *pList; pList=GetPropertyCache(L"WinNT://myComputer,computer");

IADsPropertyEntry *pEntry;
pEntry=PropertyItem(pList,L"Owner");

long ln; BSTR bstr;
pEntry->get_Name(&bstr);
printf(" Name : %S\n", bstr);

pEntry->get_ADsType(&ln);
printf(" Type : %d\n", ln);

pEntry->get_ControlCode(&ln);
printf(" Code %d\n",ln);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::Next

The IADsPropertyList::Next method gets the next item in the property list. The returned item is a Property Entry object.

HRESULT Next(
  VARIANT * pVariant
);

Parameters

pVariant
[out] Address of a caller-allocated variable that contains the value of the next item in the property list. The return value of VT_Dispatch refers to an IDispatch interface pointer to an object implementing the IADsPropertyEntry interface.

Return Values

This method supports the standard HRESULT values, including:

S_OK
The item was successfully obtained.
S_FALSE
The current item is the last one in the list. No item was obtained.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to walk through a property list using the Next method.

Dim propList As IADsPropertyList
Dim v as Variant
Dim propVal As IADsPropertyValue

On Error Resume Next

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")

propList.GetInfo
Set v = propList.Next()
While (Not (IsNull(v)) And Err.Number = 0)
    Set propEnty = v
    Debug.Print v.Name
    Debug.Print v.AdsType

    Set v = propList.Next
Wend

Example Code [C++]

The following C++ code snippet shows how to work the IADsPropertyList::Next method.

////////////////////////////////////
// function to retrieve an entry using the
// IADsPropertyList::Next method.

//     name: GetNextEntry
//    input: IADsPropertyList*
//   return: IADsPropertyEntry
//     uses: IADsPropertyList::Next
/////////////////////////////////////////////////////////
IADsPropertyEntry* GetNextEntry(IADsPropertyList* pList)
{
    VARIANT var;
    VariantInit(&var);
    IADsPropertyEntry *pEntry;

    HRESULT hr = pList->Next(&var);
    hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                         (void**)&pEntry);
    VariantClear(&var);
    return pEntry;
}

IADsPropertyList *GetPropertyCache(LPWSTR);

void TestPropertyNext()
{
    IADsPropertyList *pList;
    pList=GetPropertyCache(L"WinNT://myComputer,computer");

    IADsPropertyEntry *pEntry=GetPropertynext(&pList);

    printf("Printing from TestPropertyNext()…\n");
    BSTR bstr;
    long ln;

    hr = pEntry->get_Name(&bstr);
    printf("   Name: %S\n",bstr);
    SysFreeString(bstr);

    hr = pEntry->get_ADsType(&ln);
    printf("   Type: %d\n",ln);

    hr = pEntry->get_ControlCode(&ln);
    printf("   Code: %d\n",ln);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::PurgePropertyList

The IADsPropertyList::PurgePropertyList method deletes all items from the property list.

HRESULT PurgePropertyList(void);

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

When the PurgePropertyList method is called, all the items are removed from the cache. Thus, calling GetPropertyItem after that will generate an error.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how IADsPropertyList::PurgePropertyList method is used.

Dim propList As IADsPropertyList

Set propList = GetObject("LDAP://dc03/DC=ArcadiaBay,DC=com")
propList.GetInfo

propList.PurgePropertyList

'- None of GetPropertyItem should work, since the list is purged
'- The following line should generate error
Set propEntry = propList.GetPropertyItem("adminDescription", ADSTYPE_CASE_IGNORE_STRING)

Example Code [C++]

The following C++ code snippet shows the effect produced by a call to IADsPropertyList::PurgePropertyList.

IADsPropertyList *GetPropertyCache(LPWSTR);

void TestPurgePropertyList()
{
    IADsPropertyList *pList;
    pList=GetPropertyCache(L"WinNT://myComputer,computer");

    long count;
    pList->get_PropertyCount(&count);
    printf("Number of properties before purging: %d\n",count);

    count = -1;
    pList->PurgePropertyList();
    pList->get_PropertyCount(&count);
    printf("Number of properties after purging: %d\n",count);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::PutPropertyItem

The IADsPropertyList::PutPropertyItem method updates the values for an item in the property list.

HRESULT PutPropertyItem(
  VARIANT VarData
);

Parameters

VarData
[in] New property values to be put in the property cache.

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

You must call IADs::SetInfo to persist any changes in the directory store. The property values are not committed until the IADs::SetInfo method is called.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to add a new entry to a property list using PutPropertyItem.

Dim propList As IADsPropertyList
Set propList = GetObject("LDAP://DC=ArcadiaBay,DC=com")
Set propVal = New PropertyValue

'--- Property Value-----
propVal.CaseIgnoreString = "Arcadia Bay, Inc - Seattle, WA"
propVal.ADsType = ADSTYPE_CASE_IGNORE_STRING

'--- Property Entry ----
Set propEntry = New PropertyEntry
propEntry.Name = "adminDescription"
propEntry.Values = Array(propVal)
propEntry.ControlCode = ADS_PROPERTY_UPDATE
propEntry.ADsType = ADSTYPE_CASE_IGNORE_STRING

' --- Property List----
propList.PutPropertyItem (propEntry)

' query the IADs interface on the propList object
Dim IADsObj As IADs
Set IADsObj=propList

' Commit changes of the property list to the directory store.
IADsObj.SetInfo

Example Code [C++]

The following C++ code snippet adds a new entry to a property list using IADsPropertyList::PutPropertyItem.

// forward declaration of a helper function
HRESULT ADsBuildVarArrayDisp(IDispatch ** ppObjs,
                             DWORD      dwObjs,
                             VARIANT * pVar
                             )

int main()
{
   HRESULT hr = CoInitialize(NULL);

   IADsPropertyList *pList;
   hr = ADsOpenObject(L"LDAP://dc=ArcadiaBay, dc=com",
                      L"Administrator",
                      L"",
                      ADS_SECURE_AUTHENTICATION,
                      IID_IADsPropertyList,
                      (void**)&pList);

// create a property value object
   IADsPropertyValue *pVal;
   hr = CoCreateInstance(CLSID_PropertyValue,
                         NULL,
                         CLSCTX_INPROC_SERVER,
                         IID_IADsPropertyValue,
                         (void**)&pVal);
   hr = pVal->put_CaseIgnoreString(L"Arcadia Bay, Inc - Seattle, WA");

   hr = pVal->put_ADsType(ADSTYPE_CASE_IGNORE_STRING);

   // stuff the propertyValue object into a variant array for
   // assigment to a propertyEntry object
   IDispatch *pDisp;
   hr = pVal->QueryInterface(IID_IDispatch,(void**)&pDisp);
   hr = pVal->Release();

   VARIANT vVals;
   VariantInit(&vVals);
   hr = ADsBuildVarArrayDisp(&pDisp,1,&vVals);  //code given below.
   pDisp->Release();

   // Create a propertyEntry object
   IADsPropertyEntry *pEntry;
   hr = CoCreateInstance(CLSID_PropertyEntry,
                         NULL,
                         CLSCTX_INPROC_SERVER,
                         IID_IADsPropertyEntry,
                         (void**)&pEntry);

   hr = pEntry->put_Name(L"adminDescription");
   hr = pEntry->put_ControlCode(ADS_PROPERTY_UPDATE);
   hr = pEntry->put_ADsType(ADSTYPE_CASE_IGNORE_STRING);
   hr = pEntry->put_Values(vVals);
   VariantClear(&vVals);

   // Convert pEntry to pDisp for use in pList.PutPropertyItem
   hr = pEntry->QueryInterface(IID_IDispatch,(void**)&pDisp);
   pEntry->Release();

   VARIANT vEntry;
   VariantInit(&vEntry);
   V_DISPATCH(&vEntry)=pDisp;
   V_VT(&vEntry)=  VT_DISPATCH;
   hr = pList->PutPropertyItem(vEntry);
   VariantClear(&vEntry);

   IADs *pObj;
   hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
   pObj->SetInfo();
   pObj->Release();

   pList->Release();

   CoUninialize();
   return 0;
}

////////////////
// Helper function to build a variant array of IDispatch objects.
///////////////
HRESULT ADsBuildVarArrayDisp(
    IDispatch ** ppObjs,
    DWORD      dwObjs,
    VARIANT * pVar
    )
{

    VARIANT v;
    SAFEARRAYBOUND sabNewArray;
    DWORD i;
    SAFEARRAY *psa = NULL;
    HRESULT hr = E_FAIL;

    sabNewArray.cElements = dwObjs;
    sabNewArray.lLbound = 0;
    psa = SafeArrayCreate(VT_VARIANT, 1, &sabNewArray);

    if (!pVar) {
        hr = E_ADS_BAD_PARAMETER;
        goto Fail;
    }
    VariantInit(pVar);

    if (!psa) {
        goto Fail;
    }

    for (i = 0; i < dwObjs; i++) {
        VariantInit(&v);
        V_VT(&v) = VT_DISPATCH;
        V_DISPATCH(&v) = *(ppObjs + i);
        hr = SafeArrayPutElement(psa,
                                 (long FAR *)&i,
                                 &v
                                 );
        if (FAILED(hr))  {
            goto Fail;
        }
    }

    V_VT(pVar) = VT_VARIANT | VT_ARRAY;
    V_ARRAY(pVar) = psa;

    return(ResultFromScode(S_OK));

Fail:
    if (psa) {
        SafeArrayDestroy(psa);
    }

    return(E_FAIL);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::Reset

The IADsPropertyList::Reset method resets the list to the first item.

HRESULT Reset(void);

Return Values

This method supports the standard HRESULT values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how the Reset method is used.

Dim propList As IADsPropertyList
On Error Resume Next

Set propList = GetObject("LDAP://DC=ArcadiaBay,DC=com")

propList.GetInfo
Set v = propList.Next()
Do While (Not (IsNull(v)) And Err.Number = 0)

    Set propEnty = v
    If v.Name = "uSNCreated" Then 'Item() method could be used instead.
       Debug.Print v.Name
       Debug.Print v.ADsType
       Exit Do
    End If

    Set v = propList.Next
Loop

propList.Reset

Example Code [C++]

The following C++ code snippet shows the effect produced by a call to the IADsPropertyList::Reset method. The code of the GetPropertyCache and PropertyNext functions are listed under IADsPropertyList and IADsPropertyList::Next accordingly.

IADsPropertyList *GetPropertyCache(LPWSTR);
IADsPropertyEntry *PropertyNext(IADsPropertyList *);

void TestResetList()
{
    IADsPropertyList *pList;
    pList=GetPropertyCache(L"WinNT://kding1,computer");

    long count;
    pList->get_PropertyCount(&count);
    printf("Number of properties before reset: %d\n",count);

    printf("Walking up the property list before reset: \n");
    IADsPropertyEntry *pEntry;
    BSTR bstr;
    for (int i=0; i<count-1; i++) {
        pEntry = PropertyNext(pList);
        pEntry->get_Name(&bstr);
        printf("   Name : %S\n",bstr);
    }

    count = -1;
    pList->Reset();
    pList->get_PropertyCount(&count);
    printf("Number of properties after reset: %d\n",count);

    pEntry = PropertyNext(pList);
    pEntry->get_Name(&bstr);
    printf("    Name : %S\n",bstr);

    pEntry->Release();
    pList->Release();
    SysFreeString(bstr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::ResetPropertyItem

The IADsPropertyList::ResetPropertyItem method removes the specified item from the list (that is, from the cache). You can specify the item to be removed by name (as a string) or by index (as an integer).

HRESULT ResetPropertyItem(
  VARIANT VarData
);

Parameters

VarData
[in] Entry to be reset.

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how ResetPropertyItem is used.

Dim propList As IADsPropertyList

Set propList = GetObject("LDAP://DC=ArcadiaBay,DC=com")

'--- Now modify the cache using PutPropertyItem
Set propVal = New PropertyValue
'--- Property Value-----
propVal.CaseIgnoreString = "Arcadia-Barron Bay"
propVal.ADsType = ADSTYPE_CASE_IGNORE_STRING

'--- Property Entry ----
Set propEntry = New PropertyEntry
propEntry.Name = "adminDescription"
propEntry.Values = Array(propVal)
propEntry.ControlCode = ADS_PROPERTY_UPDATE
propEntry.ADsType = ADS_CASE_IGNORE_STRING

' --- Property List----
propList.PutPropertyItem (propEntry)

' Commit to the directory. Without this, the changes take place only in the cache.
propList.SetInfo

propList.GetInfo
Debug.Print " Number of Properties = " & propList.PropertyCount
propList.ResetPropertyItem "adminDescription"

' the property count should have been reduced by one.
Debug.Print "Number of properties = " & propList.PropertyCount

Example Code [C++]

The following C++ code snippet shows the effect produced by a call to IADsPropertylist::ResetItem. See IADsPropertyList for the listing of the GetPropertyCache function. See IADsPropertyList::Next and IADsPropertyList::Item for the listing of PropertyNext and PropertyItem, respectively.

IADsPropertyList *GetPropertyCache(LPWSTR);
IADsPropertyEntry *PropertyNext(IADsPropertyList *);
IADsPropertyEntry *PropertyItem(IADsPropertyList *,LPWSTR);

void ResetItem(IADsPropertyList *pList,LPWSTR item)
{
    VARIANT var;
    VariantInit(&var);
    V_BSTR(&var)=SysAllocString(item);
    V_VT(&var)=VT_BSTR;

    pList->ResetPropertyItem(var);
    VariantClear(&var);
}

void TestResetItem()
{
    IADsPropertyEntry *pEntry;
    IADsPropertyList *pList;
    long count;
    BSTR bstr;

    pList=GetPropertyCache(L"WinNT://myComputer,computer");

    pList->get_PropertyCount(&count);
    printf(" Count before item reset : %d\n",count);

    printf("Walking up the property list before item reset: \n");
    for (int i=0; i<count; i++) {
        pEntry = PropertyNext(pList);
        pEntry->get_Name(&bstr);
        printf("   Name : %S\n",bstr);
    }

    pList->Reset();   // move the cursor to the beginning of the list.

    ResetItem(pList,L"Owner");

    pList->get_PropertyCount(&count);
    printf(" Count after item reset : %d\n",count);

    printf("Walking up the property list after item reset: \n");

    for (i=0; i<count; i++) {
        pEntry = PropertyNext(pList);
        pEntry->get_Name(&bstr);
        printf("   Name : %S\n",bstr);
    }

    pEntry->Release();
    pList->Release();
    SysFreeString(bstr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyList::Skip

The IADsPropertyList::Skip method skips a specified number of items, counting from the current cursor position, in the property list.

HRESULT Skip(
  ULONG cElements
);

Parameters

cElements
[in] Number of elements to be skipped.

Return Values

This method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how Skip is used in selecting every other item in a property list.

Dim propList As IADsPropertyList

On Error Resume Next

Set propList = GetObject("LDAP:// DC=ArcadiaBay,DC=com")

propList.GetInfo

Set v = propList.Next()
While (Not (IsNull(v)) And Err.Number = 0)
    Set propEnty = v
    If (v.Name  = "uSNCreated") Then 'Item() method could be used instead.
        Debug.Print v.Name
        Debug.Print v.ADsType
        Exit Sub
    End If

    propList.Skip (1) 'Skip every other property
    Set v = propList.Next

Wend

Example Code [C++]

The following C++ code snippet shows the effect produced by calls to IADsPropertyList::Skip method. See IADsPropertyList interface for the code listing of the GetPropertyCache function. The listing of PropertyNext is given in IADsPropertyList::Next.

IADsPropertyList *GetPropertyCache(LPWSTR);
IADsPropertyEntry *PropertyNext(IADsPropertyList *);

void TestSkipList()
{
    IADsPropertyEntry *pEntry;
    IADsPropertyList *pList;
    long count;
    BSTR bstr;

    pList=GetPropertyCache(L"WinNT://kding1,computer");

    pList->get_PropertyCount(&count);
    printf("Walking up the property list without skipping: \n");
    for (int i=0; i<count; i++) {
        pEntry = PropertyNext(pList);
        pEntry->get_Name(&bstr);
        printf("   Name : %S\n",bstr);
    }

    pList->Reset();

    printf("Walking up the property list, skipping every other element: \n");

    for (i=0; i<count; i+=2) {
        pList->Skip(1);
        pEntry = PropertyNext(pList);
        pEntry->get_Name(&bstr);
        printf("   Name : %S\n",bstr);
    }

    pEntry->Release();
    pList->Release();
    SysFreeString(bstr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyValue

The IADsPropertyValue interface is used to represent in a property value of a property entry in the cache in one of the pre-defined data types. This interfaces exposes several properties for rendering data values in the corresponding data format. For example, IADsPropertyValue::get_LargeInteger results in a number of LargeInteger type.

A property value is held by a property entry. You can call the IADsPropertyEntry::get_Values property method to obtain the property values of an ADSI object.

When rendering property values in a format not defined in this interface, you should use the IADsPropertyValue2 interface.

Before calling the methods of this interfaces, you must call IADs::GetInfo or IADs::GetInfoEx explicitly to load the assigned property values of the object into the cache, if the cache has not been initialized. After calling the methods of this interface, you must call IADs::SetInfo to save the changes to the persistent store of the underlying directory.

The IADsPropertyValue exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPropertyValue property methods | Description
Clear | Clears the PropertyValue object's current value.
get/put_ADsType | Gets and sets a property value's data type.
get/put_DNString | Gets and sets an object's distinguished name (pathname).
get/put_CaseExactString | Gets and sets the value of a case-sensitive string.
get/put_CaseIgnoreString | Gets and sets the value of a case-insensitive string.
get/put_PrintableString | Gets and sets the value of a printable string.
get/put_NumericString | Gets and sets the value of a string consisting of numeric characters.
get/put_Boolean | Gets and sets a Boolean value.
get/put_Integer | Gets and sets an integer value.
get/put_OctetString | Gets and sets the value of a string of eight-bit characters.
get/put_SecurityDescriptor | Gets and sets a security descriptor.
get/put_LargeInteger | Gets and sets a large-integer value.
get/put_UTCTime | Gets and sets a Coordinated Universal Time value.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPropertyValue2, IADsPropertyEntry, IADsPropertyList, Property Cache


IADsPropertyValue Property Methods

The property methods of the IADsPropertyValue interface provide access to the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
ADSType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ADSType
([out] LONG *ADSType);

HRESULT put_ADSType
([in] LONG ADSType);

 The data type of the value of the property, taken from the ADSTYPE enumeration, of the value property.
DNString

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_DNString
([out] BSTR *bstrDNString);

HRESULT put_DNString
([in] BSTR bstrDNString);

 String identifying the distinguished name (path) of a directory service value object.
CaseExactString

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_CaseExactString
([out] BSTR *bstrCaseExactString);

HRESULT put_CaseExactString
([in] BSTR bstrCaseExactString);

 String to be interpreted case-sensitively.
CaseIgnoreString

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_CaseIgnoreString
([out] BSTR *bstrCaseIgnoreString);

HRESULT put_CaseIgnoreString
([in] BSTR bstrCaseIgnoreString);

 String to be interpreted without regard to case.
PrintableString

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PrintableString
([out] BSTR *bstrPrintableString);

HRESULT put_PrintableString
([in] BSTR bstrPrintableString);

 Displayable or printable string.
NumericString

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_NumericString
([out] BSTR *bstrNumericString);

HRESULT put_NumericString
([in] BSTR bstrNumericString);

 Text to be interpreted as numeral.
Boolean

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Boolean
([out] LONG *lnBoolean);

HRESULT put_Boolean
([in] LONG lnBoolean);

 Boolean value.
Integer

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Integer
([out] LONG *lnInteger);

HRESULT put_Integer
([in] LONG lnInteger);

 Integer value.
OctetString

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_OctetString
([out] BSTR *bstrOctetString);

HRESULT put_OctetString
([in] BSTR *bstrOctetString);

 Variant array of one-byte characters.
SecurityDescriptor

[Visual Basic]
Access: Read/Write
Data Type: IDISPATCH

[C++]
HRESULT get_SecurityDescriptor
([out] IDISPATCH **ppSecurityDescriptor);

HRESULT put_SecurityDescriptor
([in] IDISPATCH *pSecurityDescriptor);

 Pointer to the IADsSecurityDescriptor interface.
LargeInteger

[Visual Basic]
Access: Read/Write
Data Type: IDISPATCH

[C++]
HRESULT get_LargeInteger
([out] IDISPATCH **ppLargeInteger);

HRESULT put_LargeInteger
([in] IDISPATCH *pLargeInteger);

 Pointer to the IDispatch interface.
UTCTime

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_UTCTime
([out] DATE *daUTCTime);

HRESULT put_UTCTime
([in] DATE daUTCTime);

 A date of the VT_DATE type expressed in Coordinated Universal Time (UTC) format.

Example Code [Visual Basic]

The property methods are used to render a property value in the specified format, as defined in the previous list. For example, IADsPropertyValue::get_CaseIgnoreString returns a string value that is case insensitive. This is shown in the following Visual Basic code snippet.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim propVal As IADsPropertyValue

' get the property list
Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

' get a property entry
Set propEntry = propList.GetPropertyItem("description", ADSTYPE_CASE_IGNORE_STRING)

' print all the values of the property entry
For Each v In propEntry.Values
    Set propVal = v
    Debug.Print propVal.CaseIgnoreString
Next

Example Code [C++]

The following C++ code snippet shows how to use IADsPropertyValue::get_CaseIgnoreString to retrieve the value of the "description" property from a property list.

#include <activeds.h>
#include <stdio.h>

IADsPropertyList *pList;
IADsPropertyEntry *pEntry;
IADsPropertyValue *pVal;
IADs *pObj;
VARIANT var, varProp, varItem;
long valType = ADSTYPE_CASE_IGNORE_STRING;
BSTR valStr;
IEnumVARIANT *pEnum = NULL;
LONG lstart, lend;

VariantInit(&var);
VariantInit(&varItem);

// bind to directory object
HRESULT hr = ADsGetObject(L"LDAP://dc01/DC=ArcadiaBay,DC=com",
                          IID_IADsPropertyList,
                          (void**)&pList);

// initialize the property cache
hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
pObj->GetInfo();
pObj->Release();

// get a property entry
hr = pList->GetPropertyItem(L"description", valType, &var);
pList->Release();
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                      (void**)&pEntry);
VariantClear(&var);

// get the value array of the property entry
hr = pEntry->get_Values(&var);
SAFEARRAY *sa = V_ARRAY( &var );

// Get the lower and upper bound and iterate and print the values
hr = SafeArrayGetLBound( sa, 1, &lstart );
hr = SafeArrayGetUBound( sa, 1, &lend );
printf(" Property value(s) = ");
for ( long idx=lstart; idx < lend+1; idx++ )    {
    hr = SafeArrayGetElement( sa, &idx, &varItem );
    hr = V_DISPATCH(&varItem)->QueryInterface(IID_IADsPropertyValue,
                                              (void**)&pVal2);
    hr = pVal->get_CaseIgnoreString(&valStr);
    printf(" %S ", valStr);
    VariantClear(&varItem);
}
printf("\n");
VariantClear(&var);

See Also

ADSTYPE, IADsPropertyList, IADsPropertyEntry, IADsSecurityDescriptor, Property Cache, IADsPropertyValue2


IADsPropertyValue::Clear

The IADsPropertyValue::Clear method clears the current values of the property value object.

HRESULT IADsPropertyValue::Clear(
  void
);

Parameters

None

Return Values

This method support the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use IADsPropertyValue::Clear to clear the value of the "description" property from a property list.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim propVal As IADsPropertyValue

' get the property list
Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

' get a property entry
Set propEntry = propList.GetPropertyItem("description", ADSTYPE_CASE_IGNORE_STRING)

' print all the values of the property entry and
' clears them afterwards.
For Each v In propEntry.Values
    Set propVal = v
    Debug.Print propVal.CaseIgnoreString
    propVal.Clear
Next

Example Code [C++]

The following C++ code snippet shows how to use IADsPropertyValue::Clear to clear the value of the "description" property from a property list.

#include <activeds.h>
#include <stdio.h>

IADsPropertyList *pList;
IADsPropertyEntry *pEntry;
IADsPropertyValue *pVal;
IADs *pObj;
VARIANT var, varProp, varItem;
long valType = ADSTYPE_CASE_IGNORE_STRING;
BSTR valStr;
IEnumVARIANT *pEnum = NULL;
LONG lstart, lend;

VariantInit(&var);
VariantInit(&varItem);

// bind to directory object
HRESULT hr = ADsGetObject(L"LDAP://dc01/DC=ArcadiaBay,DC=com",
                          IID_IADsPropertyList,
                          (void**)&pList);

// initialize the property cache
hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
pObj->GetInfo();
pObj->Release();

// get a property entry
hr = pList->GetPropertyItem(L"description", valType, &var);
pList->Release();
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                      (void**)&pEntry);
VariantClear(&var);

// get the value array of the property entry
hr = pEntry->get_Values(&var);
SAFEARRAY *sa = V_ARRAY( &var );

// Get the lower and upper bound and iterate and print the values
hr = SafeArrayGetLBound( sa, 1, &lstart );
hr = SafeArrayGetUBound( sa, 1, &lend );
printf(" Property value(s) = ");
for ( long idx=lstart; idx < lend+1; idx++ )    {
    hr = SafeArrayGetElement( sa, &idx, &varItem );
    hr = V_DISPATCH(&varItem)->QueryInterface(IID_IADsPropertyValue,
                                              (void**)&pVal2);
    VariantClear(&varItem);
    hr = pVal->get_CaseIgnoreString(&valStr);
    printf(" %S ", valStr);     // write out description, if specified

    valString = L"";
    hr = pVal->Clear();
    hr = pVal->get_CaseIgnoreString(&valStr);
    printf(" %S ", valStr);        // the string is empty.
}
printf("\n");
VariantClear(&var);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyValue2

The IADsPropertyValue2 interface provides a flexible mechanism to represent property values in any data format, including new or customer-defined data types. It is also useful to deal with property values for multiple directory services. This interface defines two methods, namely, GetObjectProperty and PutObjectProperty. Both use a value-format pair as the parameters. Because IADsPropertyValue2 offers the benefit of extensibility, you should use this interface to render property values in general.

The IADsPropertyValue2 interface exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPropertyValue2 methods | Description
GetObjectProperty | Retrieves property values of an object.
PutObjectProperty | Sets property values of an object.

Inheritance: Inherited from IDispatch, IUnknown

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPropertyValue, IADsPropertyEntry, IADsPropertyList, Property Cache


IADsPropertyValue2::GetObjectProperty

The IADsPropertyValue2::GetObjectProperty method retrieves the property values of an ADSI object.

HRESULT IADsPropertyValue2::GetObjectProperty(
  long *lnADsType,
  VARIANT *pvProp
);

Parameters

lnADsType
[out, in] A pointer to the ADsType of the data referred to by pvProp. ADSI-defined data types can be found in the ADSTYPE enumeration.
pvProp
[retval, out] Pointer to property values of the VARIANT type, including VT_BSTR for strings, VT_UI1 for bytes, VT_DATE for time, and VT_DISPATCH for objects representing the property values.

Return Values

The method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

If you are not sure of the return data type, you should pass in the ADSTYPE_UNKNOWN constant as pointed to by the lnADsType pointer. In this case, ADSI will try to resolve the data types of the property value using the default data types as defined in the IADsPropertyValue interface. When you specify any other constant through lnADsType, ADSI will return the data only if the data type matches.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use IADsPropertyValue2::GetObjectProperty to retrieve the value of the property entry from a property list.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim propVal As IADsPropertyValue2
Dim descString as String

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

Set propEntry = propList.GetPropertyItem("description", ADSTYPE_CASE_IGNORE_STRING)

For Each v In propEntry.Values
    Set propVal = v
    descString = propVal.GetObjectProperty ADSTYPE_CASE_IGNORE_STRING
    MsgBox "Description: " descString
Next

Example Code [C++]

The following C++ code snippet shows how to use IADsPropertyValue2::GetObjectProperty to retrieve the value of the property entry from a property list.

#include <activeds.h>
#include <stdio.h>
IADsPropertyList *pList;
IADsPropertyEntry *pEntry;
IADsPropertyValue2 *pVal2;
IADs *pObj;
VARIANT var, varProp, varItem;
long valType = ADSTYPE_CASE_IGNORE_STRING;
IEnumVARIANT *pEnum = NULL;
LONG lstart, lend;

VariantInit(&var);
VariantInit(&varItem);
VariantInit(&varProp);

// bind to directory object
HRESULT hr = ADsGetObject(L"LDAP://dc01/DC=ArcadiaBay,DC=com",
                          IID_IADsPropertyList,
                          (void**)&pList);

// initialize the property cache
hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
pObj->GetInfo();
pObj->Release();

// get a property entry
hr = pList->GetPropertyItem(L"description", valType, &var);
pList->Release();
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                      (void**)&pEntry);
VariantClear(&var);

// get the value array of the property entry
hr = pEntry->get_Values(&var);
SAFEARRAY *sa = V_ARRAY( &var );

// Get the lower and upper bound and iterate and print the values
hr = SafeArrayGetLBound( sa, 1, &lstart );
hr = SafeArrayGetUBound( sa, 1, &lend );
printf(" Property value(s) = ");
for ( long idx=lstart; idx < lend+1; idx++ )    {
    hr = SafeArrayGetElement( sa, &idx, &varItem );
    hr = V_DISPATCH(&varItem)->QueryInterface(IID_IADsPropertyValue2,
                                              (void**)&pVal2);
    hr = pVal2->GetObjectProperty(&valType,&varProp);
    printf(" %S ", V_BSTR(&varProp));
    VariantClear(&varItem);
    VariantClear(&varProp);
}
printf("\n");
VariantClear(&var);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPropertyValue2::PutObjectProperty

The IADsPropertyValue2::PutObjectProperty method sets the property values of an ADSI object.

HRESULT IADsPropertyValue2::PutObjectProperty(
  long lnADsType,
  VARIANT pvProp
);

Parameters

lnADsType
[in] An integer to indicate the ADsType to be set to the data referred to by pvProp. ADSI-defined data types can be found in the ADSTYPE enumeration.
pvProp
[in] Property values of the VARIANT type, including VT_BSTR for strings, VT_UI1 for bytes, VT_DATE for time, and VT_DISPATCH for objects representing the property values.

Return Values

The method supports the standard HRESULT return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual basic code snippet shows how to use IADsPropertyValue2::PutObjectProperty to change a property value, descString, of the property entry in a property list.

Dim propList As IADsPropertyList
Dim propEnty As IADsPropertyEntry
Dim propVal As IADsPropertyValue2
Dim descString as String

Set propList = GetObject("LDAP://dc01/DC=ArcadiaBay,DC=com")
propList.GetInfo

Set propEntry = propList.GetPropertyItem("description", ADSTYPE_CASE_IGNORE_STRING)

descString = "This is ArcadiaBay.com"
For Each v In propEntry.Values
    Set propVal = v
    propVal.PutObjectProperty ADSTYPE_CASE_IGNORE_STRING, descString
Next
propList.Setinfo

Example Code [C++]

The following C++ code snippet shows how to use IADsPropertyValue2::PutObjectProperty to change a property value, descString, of the property entry in a property list.

#include <activeds.h>
#include <stdio.h>
IADsPropertyList *pList;
IADsPropertyEntry *pEntry;
IADsPropertyValue2 *pVal2;
IADs *pObj;
VARIANT var, varProp, varItem;
long valType = ADSTYPE_CASE_IGNORE_STRING;
IEnumVARIANT *pEnum = NULL;
LONG lstart, lend;

VariantInit(&var);

// bind to directory object
HRESULT hr = ADsGetObject(L"LDAP://dc01/DC=ArcadiaBay,DC=com",
                          IID_IADsPropertyList,
                          (void**)&pList);

// initialize the property cache
hr = pList->QueryInterface(IID_IADs,(void**)&pObj);
pObj->GetInfo();
pObj->Release();

// get a property entry
hr = pList->GetPropertyItem(L"description", valType, &var);
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsPropertyEntry,
                                      (void**)&pEntry);
VariantClear(&var);

// get the property value object
hr = pEntry->get_Values(&var);
SAFEARRAY *sa = V_ARRAY( &var );
long item = 0;
hr = SafeArrayGetElement( sa, &item, &varItem );
hr = V_DISPATCH(&varItem)->QueryInterface(IID_IADsPropertyValue2,
                                          (void**)&pVal2);
V_BSTR(&varProp) = "This is ArcadiaBay.com";
hr = pVal2->PutObjectProperty(&valType,&varProp);

// commit the change to the directory store.
hr = pList->SetInfo();
VariantClear(&var);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


Persistent Object Interfaces

This section describes the following persistent object interfaces:

  • IADsCollection
  • IADsComputer
  • IADsDomain
  • IADsFileService
  • IADsFileShare
  • IADsGroup
  • IADsLocality
  • IADsMembers
  • IADsO
  • IADsOU
  • IADsPrintjob
  • IADsPrintQueue
  • IADsService
  • IADsUser


IADsCollection

The IADsCollection interface enables its hosting ADSI object to define and manage an arbitrary set of named data elements for a directory service. Collections differ from arrays of elements in that individual items can be added or deleted without re-ordering the entire array.

Collection objects can represent one or more items that correspond to volatile information (such as processes or active communication sessions) as well as persistent data (such as physical entities for a directory service). For example, a collection object can represent a list of print jobs in a queue or a list of active sessions connected to a server. Although a collection object can represent arbitrary data sets, all the elements in a collection must be of the same type. The data are of Variant types.

ADSI also exposes the IADsMembers and IADsContainer interfaces for manipulating two special cases of collection objects. IADsMembers is used for a collection of objects who share a common membership. An example of such objects are users belonging to a group. IADsContainer applies to an ADSI object containing other objects. An example of this is a directory tree or a network topology.

The IADsCollection interface is a dual interface. It exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsCollection methods | Description
get__NewEnum | Gets an interface on an enumerator object.
Add | Adds an object to the collection.
Remove | Removes an object from the collection.
GetObject | Gets the specified item.

Remarks

Of the ADSI system providers, only the WinNT and NWCOMPAT providers support this interface to handle active file service sessions, resources and print jobs.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsCollection::Add

The IADsCollection::Add method adds a named item to the collection.

HRESULT IADsCollection::Add(
  BSTR bstrName,
  VARIANT varItem
);

Parameters

bstrName
[in] Name of the item. IADsCollection::GetObject and IADsCollection::Remove reference the item by this name.
varItem
[in] Value of the item. When the item is an object, this parameter holds the IDispatch interface pointer on the object.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The named item has been successfully added.
E_NOTIMPL
The method is not supported.

For other return values, see ADSI Error Codes.

Remarks

Collections for a directory service can also consist of a set of immutable objects.

This method is not supported in any of the ADSI system providers.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsCollection::Remove


IADsCollection::get__NewEnum

The IADsCollection::get__NewEnum method gets a dependent enumerator object that implements IEnumVARIANT for this ADSI collection object. Notice that there are two underscore characters in the function name (get__NewEnum).

HRESULT IADsCollection::get__NewEnum(
  IUnknown ** ppEnumerator
);

Parameters

ppEnumerator
[out] Indirect pointer to the IUnknown interface on the enumerator object for this collection.

Return Values

This method supports the standard return values including S_OK, E_FAIL, or E_NOTIMPL. For other return values, see ADSI Error Codes.

Remarks

When a server supports paged search and the client has specified the page limit greater than the maximum search results allowed on the server, the IADsCollection::get__NewEnum method returns errors in the following ways:

  • If the server returns an error with no results, the function returns the error only.
  • If the server returns partial results with or without an error, for example, the maximum search results allowed on the server, the function returns the partial results from the server to the user.
  • If the server returns all results with or without an error, for example, maximum search results on each page and all results through multiple pages, the function returns all the results from the server to the user.

Example Code [Visual Basic]

In Visual Basic®, the For Each …In … Next statement invokes this method implicitly. For example,

Dim fso as IADsFileServiceOperations
Set fso = GetObject("WinNT://myComputer/LanmanServer")

Dim coll as IADsCollection
Set coll = fso.Sessions

' The following statement invokes IADsCollection::get__NewEnum.
For Each session In coll
   MsgBox "Session name: " & session.Name
Next session

Example Code [C++]

The following C++ code snippet shows how IADsColection::get__NewEnum is used to enumerate active file service sessions.

HRESULT EnumCollection(IADsCollection *);

HRESULT GetACollectionOfSessions()
{
    LPWSTR adspath = L"WinNT://myComputer/LanmanServer";
    HRESULT hr;
    IADsCollection *pColl;

    // bind to file service operations
    IADsFileServiceOperations *pFso;
    hr = ADsGetObject(adspath,
                      IID_IADsFileServiceOperations,
                      (void**)&pFso);
    if(FAILED(hr)) return hr;

    // get the pointer to the collection
    hr = pFso->Sessions(&pColl);
    if(FAILED(hr)) return hr;
    pFso->Release();

    hr = EnumCollection(pColl);
    if(pColl) pColl->Release();

    return hr;
}

HRESULT EnumCollectioin(IADsCollection *pColl)
{
    IUnknown *pUnk=NULL;
    HRESULT hr;
    // get the Enumerator object on the collection object.
    hr = pColl->get__NewEnum(&pUnk);
    if (FAILED(hr) ) return hr;
    pColl->Release();

    IEnumVARIANT *pEnum;
    hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
    if (FAILED(hr)) return hr;
    pUnk->Release();

    // Now Enumerate the collection
    BSTR bstr;
    VARIANT var;
    IADs *pADs;
    ULONG lFetch;
    IDispatch *pDisp;

    VariantInit(&var);
    hr = pEnum->Next(1, &var, &lFetch);
    while(hr == S_OK)
    {
        if (lFetch == 1)
        {
             pDisp = V_DISPATCH(&var);
             pDisp->QueryInterface(IID_IADs, (void**)&pADs);
             pADs->get_Name(&bstr);
             printf("Session name: %S\n",bstr);
             SysFreeString(bstr);
             pADs->Release();
        }
        VariantClear(&var);
        pDisp=NULL;
        hr = pEnum->Next(1, &var, &lFetch);
    };
    hr = pEnum->Release();

    return S_OK;

}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IEnumVARIANT


IADsCollection::GetObject

The IADsCollection::GetObject method retrieves an item of the collection.

HRESULT IADsCollection::GetObject(
  BSTR bstrName,
  VARIANT pvarItem
);

Parameters

bstrName
[in] Name of the item. This is the same name passed to IADsCollection::Add when the item is added to the collection.
pvarItem
[in] Current value of the item. For an object, this corresponds to the IDispatch interface pointer on the object.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

If you know the name of a session in the Sessions collection, you can call the IADsCollection::GetObject method explicitly to retrieve the session object.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to retrieve a named session object (mySession) from a collection of active file service sessions.

Dim fso as IADsFileServiceOperations
Dim ses as IADsSession
Dim coll as IADsCollection

Set fso = GetObject("WinNT://myComputer/LanmanServer")
 Set coll = fso.Sessions

' The following statement invokes IADsCollection::GetObject.
Set ses = coll.GetObject(mySessionName)

Example Code [C++]

The following C++/C code snippet shows how to retrive a named session object (mySession) from a collection of active file service sessions.

HRESULT GetASessionObjectFromCollection(LPWSTR mySession)
{
    LPWSTR adspath = L"WinNT://myComputer/LanmanServer";
    IUnknown *pUnk=NULL;
    HRESULT hr;
    IADsCollection *pColl;
    IADsFileServiceOperations *pFso;

    hr = ADsGetObject(adspath,
                      IID_IADsFileServiceOperations,
                      (void**)&pFso);
    if(FAILED(hr)) return hr;

    hr = pFso->Sessions(&pColl);
    if(FAILED(hr)) return hr;
    pFso->Release();

    VARIANT varObj;
    VariantInit(&varObj);
    hr = pColl->GetObject(mySession &varObj);
    IADs *pADsObj;
    V_DISPATCH(&varObj)->QueryInterface(IID_IADs,(void**)&pADsObj);
    BSTR bstrObj;
    hr = pADsObj->get_Class(&bstrObj);
    printf("Class of the object obtained from GetObject: %S\n",
             bstrObj);
    SysFreeString(bstrObj);

    VariantClear(&varObj);
    pADsObj->Release();
    pColl->Release();

    return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IEnumVARIANT


IADsCollection::Remove

The IADsCollection::Remove method removes the named item from this ADSI collection object.

HRESULT IADsCollection::Remove(
  BSTR bstrItemToBeRemoved
);

Parameters

bstrItemToBeRemoved
[in] Name of the item as it was specified by IADsCollection::Add.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

Collections for a directory service can also consist of a set of immutable objects.

Collections that do not support direct removal of items are required to return E_NOTIMPL.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to remove a named session object (mySession) from a collection of active file service sessions.

Dim fso as IADsFileServiceOperations
Dim ses as IADsSession
Dim coll as IADsCollection

Set fso = GetObject("WinNT://myComputer/LanmanServer")
 Set coll = fso.Sessions

' The following statement invokes IADsCollection::Remove.
coll.Remove mySessionName

Example Code [C++]

The following C++/C code snippet shows how to remove a named session object (mySession) from a collection of active file service sessions.

HRESULT RemoveASessionObjectFromCollection()
{
    LPWSTR adspath = L"WinNT://myComputer/LanmanServer";
    LPWSTR mySession = L"MySession";
    HRESULT hr;
    IADsCollection *pColl;
    IADsFileServiceOperations *pFso;

    hr = ADsGetObject(adspath,
                      IID_IADsFileServiceOperations,
                      (void**)&pFso);
    if(FAILED(hr)) return hr;

    hr = pFso->Sessions(&pColl);
    if(FAILED(hr)) return hr;
    pFso->Release();

    hr = pColl->Remove(mySession);
    pColl->Release();

    return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsCollection::Add


IADsComputer

The IADsComputer interface is designed to represent and manage a computer (such as a server, client, workstation, and so on) on a network. You can manipulate the properties of this interface to access the information about a computer of interest. The information includes the operating system, the make and model, processor, computer identifier, its network addresses, and etc.

IADsComputer is a dual interface that inherits from IADs.

The IADsComputer interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsComputer property methods | Description
ComputerID | Gets and sets globally unique identifier for this machine.
Site | Gets and sets globally unique identifier for this site.
Description | Gets and sets description of this computer.
Location | Gets and sets physical location of this computer.
PrimaryUser | Gets and sets contact person for this computer.
Owner | Gets and sets licensed user of this computer.
Division | Gets and sets division to which this computer belongs.
Department | Gets and sets department to which this computer belongs.
Role | Gets and sets role of this computer (server, workstation, and so on)
OperatingSystem | Gets and sets installed operating system in use.
put_OperatingSystemVersion | Gets and sets version of installed operating system in use.
Model | Gets and sets make/model of this computer.
Processor | Gets and sets type of processor.
ProcessorCount | Gets and sets number of processors installed in this computer.
MemorySize | Gets and sets amount of RAM in MB.
StorageCapacity | Gets and sets size of disk space in MB.
NetAddresses | Gets and sets binding information.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsComputer Property Methods

The methods of the IADsComputer interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
ComputerID

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_ComputerID
([out] BSTR *pbstrComputerID);

 The globally unique identifier assigned to each machine.
Department

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Department
([out] BSTR *pbstrDepartment);

HRESULT put_Department
([in] BSTR bstrDepartment);

 The department (such as OU or organizational unit) within a company that this computer belongs to.
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The description of this computer.
Division

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Division
([out] BSTR *pbstrDivision);

HRESULT put_Division
([in] BSTR bstrDivision);

 The division (such as organization) within a company that this computer belongs to.
Location

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Location
([out] BSTR *pbstrLocation);

HRESULT put_Location
([in] BSTR bstrLocation);

 The physical location of the machine where this machine is typically located.
MemorySize

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MemorySize
([out] LONG *plMemorySize);

HRESULT put_MemorySize
([in] LONG lMemorySize);

 The size of random access memory (RAM) in megabytes (MB).
Model

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Model
([out] BSTR *pbstrModel);

HRESULT put_Model
([in] BSTR bstrModel);

 The make/model of this machine.
NetAddresses

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_NetAddresses
([out] VARIANT *pvNetAddresses);

HRESULT put_NetAddresses
([in] VARIANT vNetAddresses);

 An array of NetAddress fields that represent the addresses by which this computer can be reached. NetAddress is a provider-specific BSTR composed of two substrings separated by a colon (":"). The left-hand substring indicates the address type, and the right-hand substring is a string representation of an address of that type. For example, TCP/IP addresses are of the form "IP:100.201.301.45. IPX type addresses are of the form "IPX:10.123456.80".
OperatingSystem

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_OperatingSystem
([out] BSTR *pbstrOperatingSystem);

HRESULT put_OperatingSystem
([in] BSTR bstrOperatingSystem);

 The name of the operating system used on this machine.
OperatingSystemVersion

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_OperatingSystemVersion
([out] BSTR *pbstrOperatingSystemVersion);

HRESULT put_OperatingSystemVersion
([in] BSTR bstrOperatingSystemVersion);

 The operating system version number.
Owner

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Owner
([out] BSTR *pbstrOwner);

HRESULT put_Owner
([in] BSTR bstrOwner);

 The name of the person who typically uses this machine and has a license to run the installed software.
PrimaryUser

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PrimaryUser
([out] BSTR *pbstrPrimaryUser);

HRESULT put_PrimaryUser
([in] BSTR bstrPrimaryUser);

 The name of the contact person, in case something needs to be changed on this machine.
Processor

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Processor
([out] BSTR *pbstrProcessor);

HRESULT put_Processor
([in] BSTR bstrProcessor);

 The type of processor.
ProcessorCount

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ProcessorCount
([out] LONG *plProcessorCount);

HRESULT put_ProcessorCount
([in] LONG lProcessorCount);

 The number of processors.
Role

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Role
([out] BSTR *pbstrRole);

HRESULT put_Role
([in] BSTR bstrRole);

 The role of this machine, for example, workstation, server, domain controller.
Site

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Site
([out] BSTR *pbstrSite);

 The globally unique identifier identifying the site this machine was installed in. A site represents a physical region of good connectivity in a network.
StorageCapacity

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_StorageCapacity
([out] LONG *plStorageCapacity);

HRESULT put_StorageCapacity
([in] LONG lStorageCapacity);

 The disk space in megabytes.

Remarks

Different providers may choose to expose different properties of a computer object. For more information, see ADSI System Providers.

You can discover what properties are supported by inspecting the mandatory and optional properties through its schema class. For more information, see the IADsClass interface.

To examine the status of a computer or to perform the shutdown operation across the network, you must use the IADsComputerOperations interface.

Example Code [Visual Basic]

The following Visual Basic code snippet examines computer properties supported by the ADSI WinNT provider.

Dim obj as IADs
Set obj = GetObject("WinNT://myMachine,computer")
If (obj.Class = "Computer") Then
    MsgBox "Computer owner: " & obj.owner
    MsgBox "Computer division: " & obj.Division
    MsgBox "Computer operatingSystem: " & obj.OperatingSystem
    MsgBox "Computer operating System Version: " & obj.OperatingSystemVersion
    MsgBox "Computer processor: " & obj.Processor
    MsgBox "Computer processor Count: " & obj.ProcessorCount
End If

Example Code [C++]

The following C++/C code snippet examines computer properties supported by the ADSI WinNT provider.

IADsComputer *pComp;
LPWSTR adspath = L"WinNT://kding1,computer";
HRESULT hr;

hr = ADsGetObject(adspath,IID_IADsComputer,(void**)&pComp);
if(FAILED(hr)) return hr;

BSTR bstr;
pComp->get_Owner(&bstr);
printf("Computer owner: %S\n",bstr);
SysFreeString(bstr);

pComp->get_OperatingSystem(&bstr);
printf("Operating System: %S\n",bstr);
SysFreeString(bstr);

pComp->Release();

See Also

IADsComputerOperations


IADsDomain

The IADsDomain interface is designed for representing a network domain and for managing the accounts on the domain. This interface supports the functionality to examine whether the domain is actually a Workgroup, specify how frequent a user must change her password, and specify the maximum number of invalid password logins before lockout is set. To actually change a password, you must call the SetPassword method on an ADSI object supporting password controls. For example, to change the password of a user account, call IADsUser::SetPassword on the user object.

IADsDomain is a dual interface that inherits from IADs.

This interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsDomain property methods | Description
IsWorkgroup | Checks if the domain is actually a workstation that is a member of a workgroup.
MinPasswordLength | Gets and sets minimum number of characters required.
MinPasswordAge | Gets and sets minimum age before allowing a password change.
MaxPasswordAge | Gets and sets age at which password must be changed.
MaxBadPasswordsAllowed | Gets and sets maximum bad password logins before lockout.
PasswordHistoryLength | Gets and sets number of passwords saved.
PasswordAttributes | Gets and sets password restrictions.
AutoUnlockInterval | Gets and sets the minimum time that can elapse before an account is automatically re-enabled.
LockoutObservationInterval | Gets and sets time window for monitoring account.

Remarks

For the WinNT provider supplied by Microsoft®, this interface is implemented on the WinNTDomain object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsDomain Property Methods

The methods of the IADsDomain interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
AutoUnlockInterval

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AutoUnlockInterval
([out] LONG *plAutoUnlockInterval);

HRESULT put_AutoUnlockInterval
([in] LONG lAutoUnlockInterval);

 Indicates the minimum time elapsed before the account is automatically re-enabled.
IsWorkgroup

[Visual Basic]
Access: Read
Data Type: VARIANT

[C++]
HRESULT get_IsWorkgroup
([out] VARIANT_BOOL *retval);

 Indicates a flag that indicates whether or not the domain is a workstation belonging to a workgroup.
LockoutObservationInterval

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_LockoutObservationInterval
([out] LONG *plLockoutObservationInterval);

HRESULT put_LockoutObservationInterval
([in] LONG lLockoutObservationInterval);

 Indicates the time window during which the bad password count is monitored and accumulated before determining if the account needs to be locked out. For example, if the number of bad password attempts on an account exceed the threshold (Maximum Bad Passwords Allowed) during the specified time period (Lockout Observation Interval) the account will be locked out by setting the appropriate property in the Login Parameter property set.
MinPasswordAge

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MinPasswordAge
([out] LONG *plMinPasswordAge);

HRESULT put_MinPasswordAge
([in] LONG lMinPasswordAge);

 Indicates the minimum amount of time, in seconds, before the password is allowed to be changed.
MinPasswordLength

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MinPasswordLength
([out] LONG *plMinPasswordLength);

HRESULT put_MinPasswordLength
([in] LONG lMinPasswordLength);

 Indicates the minimum number of characters that must be typed in for a password.
MaxBadPasswordsAllowed

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MaxBadPasswordsAllowed
([out] LONG *plMaxBadPasswordsAllowed);

HRESULT put_MaxBadPasswordsAllowed
([in] LONG lMaxBadPasswordsAllowed);

 Indicates the maximum number of bad password logins until the account becomes locked out.
MaxPasswordAge

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MaxPasswordAge
([out] LONG *plMaxPasswordAge);

RESULT put_MaxPasswordAge
([in] LONG lMaxPasswordAge);

 Indicates the maximum amount of time, in seconds, after which the password must be changed by the owner.
PasswordAttributes

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_PasswordAttributes
([out] LONG *plPasswordAttributes);

HRESULT put_PasswordAttributes
([in] LONG lPasswordAttributes);

 Indicates restrictions on passwords, as defined in the following:
Attribute
PASSWORD_ATTR_NONE
PASSWORD_ATTR_MIXED_CASE
PASSWORD_ATTR_COMPLEX 		Value
0x00000000
0x00000001
0x00000002
Note For PASSWORD_ATTR_COMPLEX, the password must include at least one punctuation mark or non-printable character.
PasswordHistoryLength

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_PasswordHistoryLength
([out] LONG *plPasswordHistoryLength);

HRESULT put_PasswordHistoryLength
([in] LONG lPasswordHistoryLength);

 Indicates the number of previous passwords saved in the history list. The user cannot reuse a password that is in the history list.

Example Code [Visual Basic]

The following Visual Basic code snippet displays all the optional properties defined for a domain and then change the MinPasswordLength property value.

Dim dom as IADsDomain
Dim cls as IADsClass
Dim v
Set dom = GetObject("WinNT://myDomain")
Set cls = GetObject(dom.Schema)
For Each x in cls.OptionalProperties
    v = dom.Get(x)
    if x = "MinPasswordLength" then
        v = 10
    end if
    debug.print x & " = " & v
Next

Example Code [C++]

The following C++/C code snippet prints out the value of the MinPasswordLength property of a domain object.

LPWSTR adsPath = L"WinNT://myDomain";

// bind to the domain object
hr = ADsGetObject(adsPath,IID_IADsDomain,(void**)&pDomain);
if(FAILED(hr)) return NULL;

printf("Domain:\n");

IADs* pADsDom;
pDomain->QueryInterface(IID_IADs,(void**)&pADsDom);

VARIANT varVal;
VariantInit(&varVal);
pADsDom->Get(L"MinPasswordLength",&varVal);
printf("    MinPasswordLength = %d\n", V_I4(&varVal));

VariantClear(&varVal);
pADsDom->Release();
pDomain->Release();


IADsFileService

The IADsFileService interface is designed for representing file services supported in the directory service. Through this interface you can discover and modify the maximum number of users simultaneously running a file service.

To access active sessions or open resources used by the file service, you must go through the IADsFileServiceOperations interface to retrieve sessions or resources.

To examine the status of the file service or to perform service management operations, you use the IADsServiceOperations interface, which is inherited by IADsFileServiceOperations.

IADsFileService is a dual interface that inherits from IADsService. The IADsFileService interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsFileService property methods | Description
Description | Gets and sets the description of the file service.
MaxUserCount | Gets and sets the maximum number of users allowed to run the service concurrently.

Remarks

Under the WinNT provider, this interface is implemented on the WinNTService object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsFileService Property Methods

The property methods of the IADsFileService interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The description of the file service.
MaxUserCount

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MaxUserCount
([out] LONG *plMaxUserCount);

HRESULT put_MaxUserCount
([in] LONG lMaxUserCount);

 The maximum number of users allowed on the service at any time.

Remarks

You must go through the file service to access file shares, sessions, and resources on a computer.

Example Code [Visual Basic]

The following Visual Basic code snippet writes a description for and check the user limit of the file service.

Dim fs As IADsFileService

'  Bind to a file service object on "myComputer" in the local domain.
Set fs = GetObject("WinNT://myComputer/LanmanServer")

fs.Description = "WinNT file service."
n = fs.MaxUserCount
if n = -1 then
   MsgBox "No limit has been imposed on number of users allowed."
else
   MsgBox n & " users are allowed."
end if

Example Code [C++]

The following C++/C code snippet writes a description for and check the user limit on a file service object.

HRESULT CheckFileService()
{
    IADsFileService *pFs;
    LPWSTR adsPath = L"WinNT://myComputer/LanmanServer";
    HRESULT hr;
    hr = ADsGetObject(adsPath, IID_IADsFileService, (void**)&pFs)
    if (FAILED(hr)) return hr;
    hr = pFs->put_Description(L"WinNT File Service");
    hr = pFs->SetInfo();

    long count;
    hr = pFs->get_MaxUserCount(&count);
    if(count == -1) {
        printf("No limit has been imposed on the number of users.\n");
    }
    else {
        printf("Number of allowed users are %d\n",count);
    }
    return S_OK;
}

See Also

IADsService, IADsFileServiceOperations, IADsServiceOperations


IADsFileShare

The IADsFileShare interface is designed for representing a published file share across the network. Call the methods on IADsFileShare when you want to access or publish information about a file share point.

To bind to a file share, you must bind to "LanManServer" on the host machine of interest. For example,

Dim fs as IADsFileShare
Set fs = GetObject("WinNT://aComputer/LanmanServer")

The IADsFileShare interface is a dual interface that inherits from IADs.

This interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets andsets one of its properties.

IADsFileShare property methods | Description
CurrentUserCount | Gets the current number of users connected to this share.
Description | Gets and sets the description of the file share.
HostComputer | Gets and sets the ADsPath reference to the host computer.
Path | Gets and sets the file system path to shared directory.
MaxUserCount | Gets and sets the maximum number of users allowed to access share at any one time.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsFileShare Property Methods

The property methods of the IADsFileshare interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
CurrentUserCount

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_CurrentUserCount
([out] LONG *plCurrentUserCount);

 The number of users connected to the share.
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The description of the file share.
HostComputer

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HostComputer
([out] BSTR *pbstrHostComputer);

HRESULT put_HostComputer
([in] BSTR bstrHostComputer);

 An ADsPath reference to the host computer.
MaxUserCount

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_MaxUserCount
([out] LONG *plMaxUserCount);

 The maximum number of users allowed to access the share at one time.
Path

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Path
([out] BSTR *pbstrPath);

HRESULT put_Path
([in] BSTR bstrPath);

 The file system path to the shared directory.

Example Code [Visual Basic]

To access the properties of file shares on a computer, you must first bind to the "LanmanServer" on the machine. The following Visual Basic code snippet illustrates how to set up the description and the maximum number of allowed users for all the public file shares on the computer, named as "myMachine", in the default domain.

Dim fs As IADsFileService
Dim fish As IADsFileShare
Set fs = GetObject("WinNT://myMachine/LanmanServer")
If (fs.class = "FileService") then
    For Each fish In fs
        fish.description = fish.name & " is my working folder."
        fish.MaxUserCount =  10
        fish.SetInfo
    Next fish
End if

The following code snippet shows how to make the existing C:\MyFolder directory a public file share.

Dim fs As IADsFileShare
Dim cont As IADsContainer

Set cont = GetObject("WinNT://yourDomain/yourMachineName/LanmanServer")

Set fs = cont.Create("FileShare", "Public")
Debug.Print fs.Class
fs.Path = "C:\MyFolder"
fs.SetInfo

Presently, you cannot set the permission on the share.

Example Code [C++]

The following C++/C code snippet makes the existing C:\MyFolder directory a public file share.

IADsFileShare *pFish;
IADsContainer *pCont;
LPWSTR adsPath = L"WinNT://mimmo/lanmanServer";

HRESULT hr;
hr = ADsGetObject(adsPath, IID_IADsContainer,(void**)&pCont);
if(FAILED(hr)) exit( 0 );

hr = pCont->Create(L"FileShare",L"Public", (IDispatch**)&pFish);
pCont->Release();
if(FAILED(hr)) exit(0);

hr = pFish->put_Path(L"c:\\public");
if(FAILED(hr) ) {
    pFish->Release();
    exit(0);
}

hr = pFish->SetInfo();
pFish->Release();


IADsGroup

The IADsGroup interface is designed to represent and manage group membership information from a directory service. It allows you to retrieve member objects, to test if a given object belongs to the group, and to add (or remove) an object to (or from) the group.

IADsGroup is a dual interface that inherits from IADs.

This interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsGroup properties and methods | Description
Description | Gets and sets the description of the group membership.
Members | Gets an IADsMembers interface on the members object that is the collection of ADSI objects that represent the members of this group.
IsMember | Tests for membership.
Add | Adds an object to a group.
Remove | Removes an object from a group.

Example Code [Visual Basic]

To create a group object, call GetObject(adspath), where adspath is the path to the group of interest. For example, you can bind to the Power Users group as shown in the following code snippet for the WinNT provider:

Dim group As IADsGroup
Set group = GetObject("WinNT://ArcadiaBay/Power Users")

The following code snippet works with the LDAP provider:

Dim group As IADsGroup
Set group = GetObject("LDAP://CN=Power Users,DC=ArcadiaBay,DC=com")

Example Code [C++]

The following C++/C code snippet binds to the Power Users group through the WinNT provider.

IADsGroup *pGroup;
HRESULT hr;
LPWSTR adsPath = L"WinNT://ArcadiaBay/Power Users";

hr = ADsGetObject(adsPath,IID_IADsGroup,(void**)&pGroup);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsGroup Property Methods

The property methods of the IADsGroup interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 Indicates the textual description of the group membership.

Example Code [Visual Basic]

Dim grp As IADsGroup
Set grp = GetObject("WinNT://ArcadiaBay/Administrators")
Debug.Print grp.Description

Example Code [C++]

The following C++/C code snippet binds to a group object and displays the description of the group.

IADsGroup *pGroup;
HRESULT hr;
LPWSTR adsPath = L"WinNT://localHost/Administrators";

hr = ADsGetObject(adsPath,IID_IADsGroup,(void**)&pGroup);

if(FAILED(hr)) exit(hr);

BSTR bstr;
hr = pGroup->get_Description(&bstr);
if(FAILED(hr)) exit(hr);

printf("Descripton: %S\n",bstr);
SysFreeString(bstr);

pGroup->Release();


IADsGroup::Add

The IADsGroup::Add method adds an ADSI object to an existing group.

HRESULT IADsGroup::Add(
  BSTR bstrNewItem
);

Parameters

bstrNewItem
[in] ADsPath of the object being added to the group.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to add a user object ("jsmith") to the group ("Administrators") on the "ArcadiaBay" domain, using the WinNT provider.

Dim grp As IADsGroup
Set grp = GetObject("WinNT://ArcadiaBay/Administrators")
grp.Add ("WinNT://ArcadiaBay/jsmith")

The following Visual Basic code snippet illustrates how to add a user object to a group using the LDAP provider.

Dim grp As IADsGroup
Set grp = GetObject("LDAP://CN=Administrators, CN=Users, DC=ArcadiaBay, DC=com")
grp.Add("LDAP://CN=John Smith, OU=Sales,DC=ArcadiaBay,DC=com")

Example Code [C++]

The following C++/C code snippet adds an existing user account to the Administrators group.

IADsGroup *pGroup;
HRESULT hr;
LWPSTR adsPath = L"WinNT://ArcadiaBay/Administrators";
hr = ADsGetObject(adsPath,IID_IADsGroup,(void**)&pGroup);
if(FAILED(hr)) exit(hr);

// assuming the "WinNT://ArcadiaBay/jsmith" user account exists
// and does not already belong to the Administrators group.

hr = pGroup->Add("WinNT://ArcadiaBay/jsmith");
if(FAILED(hr)) exit(hr);

pGroup->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsMembers


IADsGroup::IsMember

The IADsGroup::IsMember method determines if an ADSI object is a member of the group representing the object implementing this interface.

HRESULT IADsGroup::IsMember(
  BSTR bstMember,
  VARIANT_BOOL * bMember
);

Parameters

pbstrName
[in] ADsPath name of a user account object to be checked for membership in the group object that supports this interface method.
bMember
[out] TRUE if this object is a member of this group.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code adds the "jsmith" user to the "Administrators" group on the "ArcadiaBay" domain, and then reports that the user is now a member of the group.

Dim grp As IADsGroup
Set grp = GetObject("WinNT://ArcadiaBay/Administrators")
grp.Add ("WinNT://ArcadiaBay/jsmith")
Debug.Print grp.IsMember("WinNT://ArcadiaBay/jsmith ") 'Should be TRUE

When supported, this method can be used for a recursive test of whether a group is allowed to have other groups as members.

Example Code [C++]

The following C++ code snippet checks if a user belongs to a group before adding it to the group.

IADsGroup *pGroup;
HRESULT hr;
LPWSTR adsPath = L"WinNT://AcardiaBay/Administrators";

hr = ADsGetObject(adsPath,IID_IADsGroup,(void**)&pGroup);

if(FAILED(hr)) exit(hr);

BSTR bstr;
hr = pGroup->get_Description(&bstr);
if(FAILED(hr)) exit(hr);

printf("Descripton: %S\n",bstr);
SysFreeString(bstr);

VARIANT_BOOL inG=false;
hr = pGroup->IsMember(L"WinNT//ArcadiaBay/SecUser", &inG);

if (inG ) {
    printf("already in the group.\n");
}
else {
    hr = pGroup->Add(L"WinNT://ArcadiaBay/SecUser");
    printf("user added.\n");
}

pGroup->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsMembers


IADsGroup::Members

The IADsGroup::Members method retrieves all the members of the group. It gets the IADsMembers interface on an ADSI collection object that represents the members of this group.

HRESULT IADsGroup::Members(
  IADsMembers ** ppMembers
);

Parameters

ppMembers
[out] Pointer to the IADsMembers interface that supports methods to identify, manage, and select members of this group.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet enumerates all the members of a group.

Dim grp As IADsGroup
Dim memberList As IADsMembers
Dim member As IADs

Set grp = GetObject("WinNT://ArcadiaBay/Administrators")
Set memberList = grp.members
For Each member In memberList
   Debug.Print member.Name & "(" & member.Class & ")"
Next

Example Code [C++]

The following C++ code snippet enumerates all members of a group.

HRESULT EnumerateGroupMembers(IADsGroup *pGroup)
{
    IADsMembers *pMembers;
    HRESULT hr = S_OK;
    hr = pGroup->Members(&pMembers);
    pGroup->Release();
    if (FAILED(hr)) return hr;

    hr = EnumMembers(pMembers);  // See code snippet in
                                 // IADsMembers::get__NewEnum.
    Pmembers->Release();
    return hr;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsMembers


IADsGroup::Remove

The IADsGroup::Remove method removes the specified user object from this group. The operation does not remove the group object itself even when there is no member left in the group.

HRESULT IADsGroup::Remove(
  BSTR bstrItemToBeRemoved
);

Parameters

bstrItemToBeRemoved
[in] ADsPath of the object to be removed from the group.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following is a Visual Basic code snippet that removes a user account from a group.

Dim grp As IADsGroup
Set grp = GetObject("WinNT://ArcadiaBay/Administrators")
grp.Remove ("WinNT://ArcadiaBay/jsmith")

Example Code [C++]

The following C++ code snippet removes a user from a group.

IADsGroup *pGroup;
HRESULT hr;
LPWSTR usrPath = L"WinNT://ArcadiaBay/jsmith";
LPWSTR grpPath = L"WinNT://ArcadiaBay/Administrators";

hr = ADsGetObject(grpPath, IID_IADsGroup, (void**)&pGroup);
if(FAILED(hr)) exit(hr);

hr = pGroup->Remove(usrPath);
if(FAILED(hr)) exit(hr);

pGroup->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsMembers


IADsLocality

The IADsLocality interface is designed for representing the geographical location, or region, of a directory entity. This interface is one of several that provide support for directory services to organize accounts by country, locality (region), organization (company), or organizational unit (department). This interface manages locality, the IADsO interface manages organization, and the IADsOU interface manages the organization unit.

When a directory service provides hierarchical groupings of directory entries by country, locality, organization, or organization unit, you can use this and the related interfaces to expand the directory tree accordingly. In this case, the IADsLocality interface is implemented by a locality object that implements the IADsContainer interface as well.

IADsLocality is a dual interface that inherits from IADs. The IADsLocality interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsLocality property methods | Description
get/put_Description | Gets and sets the description of the geographical region as represented by this object.
get/put_LocalityName | Gets and sets the name of this region.
get/put_PostalAddress | Gets and sets the main post office address of this region.
get/put_SeeAlso | Gets and sets any other information relevant to this region.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsLocality Property Methods

The methods of the IADsLocality interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 Indicates the text that describes the locality.
LocalityName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LocalityName
([out] BSTR *pbstrLocalityName);

HRESULT put_LocalityName
([in] BSTR bstrLocalityName);

 Indicates the name of the geographical region as represented by this locality object.
PostalAddress

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PostalAddress
([out] BSTR *pbstrPostalAddress);

HRESULT put_PostalAddress
([in] BSTR bstrPostalAddress);

 Indicates the main postal address of the locality.
SeeAlso

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_SeeAlso
([out] BSTR *pbstrSeeAlso);

HRESULT put_SeeAlso
([in] BSTR bstrSeeAlso);

 Indicates an array of ADsPath names of other directory objects which may be relevant to this object.

Example Code [Visual Basic]

The following Visual Basic code snippet displays the locality information of a container object. It assumes that a locality object, named "myLocality", has been created for the container object and the properties have been set.

Dim dom as IADsContainer
Dim loc as IADsLocality

Set dom = getObject("LDAP://adsrv1/dc=ArcadiaBay, dc=Com")
Set loc = dom.GetObject("locality","L=myLocality")
Debug.Print loc.Name
Debug.Print loc.LocalityName
Debug.Print loc.Description
Debug.Print loc.PostalAddress
Debug.Print loc.SeeAlso


IADsMembers

The IADsMembers interface is designed for managing a list of ADSI object references. It is implemented to support group membership for individual accounts. It can be used to manage a collection of ADSI objects belonging to a group. To access the collection of group members, use the IADsGroup::get_Members property method implemented by the ADSI group object.

The IADsMembers interface serves a slightly different purpose from the IADsCollection and IADsContainer interfaces, which also works with a set of data or objects. IADsCollection manages sets of arbitrary data elements that are not object references, whereas IADsContainer manages objects that are part of the directory tree structure or the network topology.

IADsMembers is a dual interface. It exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsMembers property methods | Description
get_Count | Gets the number of members.
get__NewEnum | Retrieves an interface on an enumerator object.
get/put_Filter | Gets and sets the filter for selection.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsMembers Property Methods

The methods of the IADsMembers interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
Count

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Count
([out] LONG *plCountr);

 Indicates the number of items in the container. If the filter is set then count returns only the number of items that fit the filter description.
Filter

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Filter
([out] VARIANT *pvFilter);

HRESULT put_Filter
([in] VARIANT vFilter);

 Indicates the filter. The syntax of the entries in the filter array is the same as the Filter used on the IADsContainer interface.

Remarks

The ADSI system providers do not support the IADsMembers::get_Count property method.

Example Code [Visual Basic]

The following Visual Basic code snippet provides an example of using the property methods of this interface.

Dim grp as IADsGroup
Set grp = GetObject("WinNT://myComputer/someGroup")
grp.members.filter = Array("user")
For each usr in grp.members
    MsgBox usr.Name & "," & usr.Class & "," & usr.AdsPath
Next

Example Code [C++]

The following C++ code snippet uses the IADsMembers::get_Filter method to select the collection of users.

IADsGroup *pGroup;
HRESULT hr;

LPWSTR grpPath = L"WinNT://myComputer/someGroup";
hr = ADsGetObject(grpPath,IID_IADsGroup,(void**)&pGroup);
if(FAILED(hr)) exit(hr);

IADsMembers *pMembers;
hr = pGroup->Members(&pMembers);
if(FAILED(hr)) exit(hr);
hr = pGroup->Release();

SAFEARRAY *sa = CreateSafeArray(L"user");
hr = pMembers->put_Filter(sa);

hr = EnumMembers(pMembers);    // See code snippet in
                              // IADsMembers::get__NewEnum.
if(pMembers) pMembers->Release();


IADsMembers::get__NewEnum

The IADsMembers::get__NewEnum method gets a dependent enumerator object that implements IEnumVARIANT for this ADSI collection object. Note that there are two underscore characters in the function name (get__NewEnum).

HRESULT IADsMembers::get__NewEnum(
  IUnknown ** ppEnumerator
);

Parameters

ppEnumerator
[out] Indirect pointer to the IUnknown interface on the enumerator object for this collection.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet enumerates members of collection.

Dim grp As IADsGroup
Set grp = GetObject("WinNT://ArcadiaBay/SaleGroup")
For Each member in Grp
    MsgBox member.Name
Next member

Example Code [C++]

The following C++ code snippet enumerates members of any collection object that supports the IADsMembers interface.

HRESULT EnumMembers(IADsMembers *pMembers)
{
    IUnknown *pUnk;
    HRESULT hr;
    hr = pMembers->get__NewEnum(&pUnk);
    if (FAILED(hr)) return hr;
    pMembers->Release();

    IEnumVARIANT *pEnum;
    hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
    if (FAILED(hr)) return hr;

    pUnk->Release();

    // Now Enumerate
    BSTR bstr;
    VARIANT var;
    IADs *pADs;
    ULONG lFetch;
    IDispatch *pDisp;

    VariantInit(&var);
    hr = pEnum->Next(1, &var, &lFetch);
    while(hr == S_OK)
    {
        if (lFetch == 1)
        {
             pDisp = V_DISPATCH(&var);
             pDisp->QueryInterface(IID_IADs, (void**)&pADs);
             pADs->get_Name(&bstr);
             printf("MEMBERS: %S\n",bstr);
             SysFreeString(bstr);
             pADs->Release();
        }
        VariantClear(&var);
        pDisp=NULL;
        hr = pEnum->Next(1, &var, &lFetch);
    };
    hr = pEnum->Release();
    return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IEnumVARIANT


IADsO

The IADsO interface is designed for representing and managing the organization to which an account belongs. This interface is one of several that provide support for directory services to organize accounts by country, locality (region), organization (company), and organizational unit (department). Organization is managed by this interface, locality by the IADsLocality interface, and organization unit by IADsOU.

When a directory service provides hierarchical groupings of directory entries by country, locality, organization, and organization unit, you can use this, and the related interfaces, to expand the directory tree accordingly. In this case, the IADsO interface is implemented by an organization object that implements the IADsContainer interface as well.

IADsO is a dual interface that inherits from IADs. The IADsO interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsO property methods | Description
get/put_Description | Gets and sets the description of the organization, such as the company name.
get/put_LocalityName | Gets and sets the name of the physical location of the organization.
get/put_PostalAddress | Gets and sets the postal address of the organization.
get/put_TelephoneNumber | Gets and sets the telephone number of the organization.
get/put_FaxNumber | Gets and sets the fax number of the organization.
get/put_SeeAlso | Gets and sets the other information relevant to this organization.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsO Property Methods

The property methods of the IADsO interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The description of the organization.
FaxNumber

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_FaxNumber
([out] BSTR *pbstrFaxNumber);

HRESULT put_FaxNumber
([in] BSTR bstrFaxNumber);

 The fax number of the organization.
LocalityName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LocalityName
([out] BSTR *pbstrLocalityName);

HRESULT put_LocalityName
([in] BSTR bstrLocalityName);

 The name of the region in which the organization is located.
PostalAddress

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PostalAddress
([out] BSTR *pbstrPostalAddress);

HRESULT put_PostalAddress
([in] BSTR bstrPostalAddress);

 The postal address of the organization.
TelephoneNumber

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_TelephoneNumber
([out] BSTR *pbstrTelephoneNumber);

HRESULT put_TelephoneNumber
([in] BSTR bstrTelephoneNumber);

 The telephone number of the organization.
SeeAlso

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_SeeAlso
([out] BSTR *pbstrSeeAlso);

HRESULT put_SeeAlso
([in] BSTR bstrSeeAlso);

 An array of ADsPath names of other ADSI objects which may be relevant to this object.

Example Code [Visual Basic]

The following Visual Basic code snippet displays the description of a given organization. It assumes that the underlying directory service supports grouping directory objects by organization.

Dim dom as IADsContainer
Dim dso as IADsOpenDSObject

Set dso = GetObject("LDAP:")
Set dom = dso.OpenDSObject("LDAP://adsrv1/dc=ArcadiaBay, dc=Com", "jsmith","passwd",1)
dom.Filter = Array("organization")
For each o in dom
    MsgBox "Fax number of " & o.Name & " : " & o.Description
Next


IADsOU

The IADsOU interface is designed for representing and managing the organizational unit to which an account belongs. This interface is one of several that provide support for directory services to organize accounts by country, locality (region), organization (company), and organizational unit (department). Organization unit is managed by this interface, locality by the IADsLocality interface, and organization by IADsO.

When a directory service provides hierarchical groupings of directory entries by country, locality, organization, and organization unit, you can use this and the related interfaces to expand the directory tree accordingly. In this case, the IADsOU interface is implemented by an organization unit object that implements the IADsContainer interface as well.

IADsOU is a dual interface that inherits from IADs. The IADsOU interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsOU property methods | Description
get/put_Description | Gets and sets the description of the organizational unit.
get/put_LocalityName | Gets and sets the physical location of the unit.
get/put_PostalAddress | Gets and sets the post office address of the unit.
get/put_TelephoneNumber | Gets and sets the telephone number of the unit.
get/put_FaxNumber | Gets and sets the fax number of the unit.
get/put_SeeAlso | Gets and sets the other information relevant to the unit.
get/put_BusinessCategory | Gets and sets the category of general business functions of the unit.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsOU Property Methods

The property methods of the IADsOU interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
BusinessCategory

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_BusinessCategory
([out] BSTR *pbstrBusinessCategory);

HRESULT put_BusinessCategory
([in] BSTR bstrBusinessCategory);

 A string indicating the general business function(s) performed by the organizational unit, for example "Accounting".
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The text that describes the organization unit.
FaxNumber

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_FaxNumber
([out] BSTR *pbstrFaxNumber);

HRESULT put_FaxNumber
([in] BSTR bstrFaxNumber);

 The fax number of the organization unit.
LocalityName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LocalityName
([out] BSTR *pbstrLocalityName);

HRESULT put_LocalityName
([in] BSTR bstrLocalityName);

 The name of the geographical region of the organization unit.
PostalAddress

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PostalAddress
([out] BSTR *pbstrPostalAddress);

HRESULT put_PostalAddress
([in] BSTR bstrPostalAddress);

 The postal address of the organization unit.
TelephoneNumber

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_TelephoneNumber
([out] BSTR *pbstrTelephoneNumber);

HRESULT put_TelephoneNumber
([in] BSTR bstrTelephoneNumber);

 The telephone number of the organization unit.
SeeAlso

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_SeeAlso
([out] BSTR *pbstrSeeAlso);

HRESULT put_SeeAlso
([in] BSTR bstrSeeAlso);

 An array of ADsPath names of other directory objects which may be relevant to this object.

Example Code [Visual Basic]

The following Visual Basic code snippet displays the description of a given organization unit. It assumes that the underlying directory service supports grouping of directory objects by organization unit.

Dim dom as IADsDomain
Dim ou as IADsOU
Set dom = GetObject("LDAP://server1/domain1")
dom.filter = Array("organizationalUnit")
For each ou in dom
    MsgBox ou.BusinessCategory & ": " & ou.Description
Next


IADsPrintJob

The IADsPrintJob interface is designed for representing a print job. When a user submits a request to a printer to print a document, a print job is created in the print queue. The property methods allow you to access the information about a print job. Such information includes which printer does the printing, who submitted the document, when it was submitted, and how many pages will be printed.

To manage a print job across a network, use the IADsPrintJobOperations interface, which supports the functionality to examine the status of a print job and to pause or resume the operation of printing the document, and so on.

To access any print jobs in a print queue, call the IADsPrintQueueOperations::PrintJobs method to obtain the collection object holding all the print jobs in the print queue.

IADsPrintJob is a dual interface that inherits from IADs. The IADsPrintJob interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPrintJob property methods | Description
get_HostPrintQueue | Gets the ADsPath string that names the print queue processing this print job.
get_User | Name of user submitting print job.
get_UserPath | Gets the ADsPath name for user to submit the print job.
get_TimeSubmitted | Gets the time when the job was submitted to the print queue.
get_TotalPages | Gets the total number of pages in the print job.
get_Size | Gets the size of the print job in bytes.
get/put_Description | Gets and sets the description of the print job.
get/put_Priority | Gets and sets the priority of the print job.
get/put_StartTime | Gets and sets the earliest time when the print job should be started.
get/put_UntilTime | Gets and sets the time when the print job should be stopped.
get/put_Notify | Gets and sets the user to be notified when job is completed.
get/put_NotifyPath | The ADsPath string for user to be notified when the job is completed.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to manage a print job submitted to the printer, "\\aMachine\aPrinter".

Dim pq as IADsPrintQueue
Dim pqo as IADsPrintQueueOperations
Dim pj as IADsPrintJob
Dim pjo as IADsPrintJobOperations
Dim pjs as IADsCollection

Set pq = GetObject("WinNT://aMachine/aPrinter")
Set pqo = pq
For Each pj in pqo.PrintJobs
   MsgBox pj.class
   MsgBox pj.description
   MsgBox pj.HostPrintQueue
   Set pjo = pj
   if Hex(pjo.status) = 10 ' printing
      pjo.Pause
   else
      pjo.Resume
   end if
Next

Example Code [C++]

The following C++ code snippet shows how to manage a print job submitted to the printer, "\\aMachine\aPrinter".

IADsPrintJobOperations *pjo;
IADsPrintQueueOperations *pqo;

HRESULT hr ;
hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                  IID_IADsPrintQueueOperations,
                  (void**)&pqo);
if (FAILED(hr)) exit(hr);

IADsCollection *pColl;
hr = pqo->PrintJobs(&pColl);

IUnknown *pUnk = NULL;
hr = pColl->get__NewEnum(&pUnk);
pColl->Release();
if (FAILED(hr) ) exit(hr);

IEnumVARIANT *pEnum;
hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
pUnk->Release();
if (FAILED(hr)) exit(hr);

// Now Enumerate
VARIANT var;
ULONG lFetch;
IDispatch *pDisp;

long status;

VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADsPrintJobOperations,
                              (void**)&pjo);

        pjo->get_Status(&status);
        printf("Job status: %x\n",status);
        if(stats == ADS_JOB_PRINTING) {
            pjo.Pause();
        }
        else {
            pjo.Resume();
        }
        pjo->Release();
    }
    VariantClear(&var);
    pDisp=NULL;
    hr = pEnum->Next(1, &var, &lFetch);
};
hr = pEnum->Release();

hr = pqo->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPrintJob Property Methods

Property methods for the IADsPrintJob interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The description of the print job.
HostPrintQueue

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_HostPrintQueue
([out] BSTR *pbstrHostPrintQueue);

 The ADsPath string of the Print Queue that is processing the print job.
Notify

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Notify
([out] BSTR *pbstrNotify);

HRESULT put_Notify
([in] BSTR bstrNotify);

 The user to be notified when job is completed.
NotifyPath

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_NotifyPath
([out] BSTR *pbstrNotifyPath);

HRESULT put_NotifyPath
([in] BSTR bstrNotifyPath);

 The ADsPath string of the user object to be notified when the print job is completed.
Priority

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Priority
([out] LONG *plPriority);

HRESULT put_Priority
([in] LONG lPriority);

 The priority of the print job.
Size

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Size
([out] LONG *plSize);

 The size of the print job in bytes.
StartTime

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_StartTime
([out] DATE *pdateStartTime);

HRESULT put_StartTime
([in] DATE dateStartTime);

 The earliest time when the print job should be started.
TimeSubmitted

[Visual Basic]
Access: Read
Data Type: DATE

[C++]
HRESULT get_TimeSubmitted
([out] DATE *pdateTimeSubmitted);

 The time when the job was submitted to the queue.
TotalPages

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_TotalPages
([out] LONG *plTotalPages);

 The total number of pages in the print job.
UntilTime

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_UntilTime
([out] DATE *pdateUntilTime);

HRESULT put_UntilTime
([in] DATE dateUntilTime);

 The latest time when the print job should be started.
User

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_User
([out] BSTR *pbstrUser);

 The name of user who submitted the print job.
UserPath

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_UserPath
([out] BSTR *pbstrUserPath);

 The ADsPath string of the user object that submitted this print job.

Example Code [Visual Basic]

The following Visual Basic code example shows how to work with properties of a print job object.

Dim pqo as IADsPrintQueueOperations
Dim pj as IADsPrintJob
Set pqo = GetObject("WinNT://aMachine/aPrinter")
For Each pj in pqo.PrintJobs
    MsgBox "Host Printer: " & pj.HostPrintQueue
    MsgBox "Job description: " & pj.Description
    MsgBox "job requester: " & pj.User
Next

Example Code [C++]

The following C++ code snippet shows how to work with properties of a print job object.

IADsPrintQueueOperations *pqo;
IADsPrintJob *pJob;
HRESULT hr ;
LPWSTR adsPath =L"WinNT://aMachine/aPrinter";
hr = ADsGetObject(adsPath,
                  IID_IADsPrintQueueOperations,
                  (void**)&pqo);
if (FAILED(hr)) exit(hr);

IADsCollection *pColl;
hr = pqo->PrintJobs(&pColl);

// now to enumerate the print jobs. Code omitted.

IUnknown *pUnk = NULL;
hr = pColl->get__NewEnum(&pUnk);
pColl->Release();
if (FAILED(hr) ) exit(hr);

IEnumVARIANT *pEnum;
hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
pUnk->Release();
if (FAILED(hr)) exit(hr);

// Now Enumerate
BSTR bstr;
VARIANT var;
ULONG lFetch;
IDispatch *pDisp;

VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADsPrintJob, (void**)&pJob);

        pJob->get_HostPrintQueue(&bstr);
        printf("HostPrintQueue: %S\n",bstr);
        SysFreeString(bstr);

        pJob->get_Description(&bstr);
        printf("Print job name: %S\n",bstr);
        SysFreeString(bstr);

        pJob->get_User(&bstr);
        printf("Requester: %S\n",bstr);
        SysFreeString(bstr);

        pJob->Release();
    }
    VariantClear(&var);
    pDisp=NULL;
    hr = pEnum->Next(1, &var, &lFetch);
};

hr = pEnum->Release();

hr = pqo->Release();

See Also

IADsPrintJobOperations, IADsPrintQueueOperations::PrintJobs


IADsPrintQueue

The IADsPrintQueue interface represents a printer on a network. The property methods of this interface allow you to access the about a printer. Such information includes the printer model, its physical location, the network address, and so on. You can use this interface to browse through a collection of print jobs in the print queue. To control a printer across a network, you use the IADsPrintQueueOperations interface. To obtain a collection of the print jobs, call the IADsPrintQueueOperations::PrintJobs method.

In Windows NT®/Windows® 2000, a printer, or a print queue, is managed by a host computer. If the path to a print queue is known, you can bind to it as you would to any other ADSI objects. For example,

Dim pq as IADsPrintQueue
Set pq = GetObject("WinNT://aMachine/aPrinter")

or in C++,

IADsPrintQueue *pq;
LPWSTR adsPath = L"WinNT://aMachine/aPrinter";
HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsPrintQueue,
                          (void**)&pq);

To enumerate all print queues on a given computer, proceed as follows:

  1. Bind to the computer object.
  2. Determine if the computer contains any "PrintQueue" objects.
  3. Walk through all the found printer objects.

The following code snippet enumerates all the printers on a given computer.

    Dim cont As IADsContainer
    Dim pq As IADsPrintQueue

    ' Bind to the computer object
    Set cont = GetObject("WinNT://romulus,computer")

    cont.Filter = Array("PrintQueue")
    For Each p In cont
       Set pq = GetObject(p.ADsPath)
       MsgBox pq.Name & " is a " & pq.Model
    Next p

IADsPrintQueue is a dual interface that inherits from IADs. The IADsPrintQueue interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPrintQueue property methods | Description
get/put_PrinterPath | Gets and sets the path where a shared printer can be accessed.
get/put_Model | Gets and sets the name of the driver used by this print queue.
get/put_Datatype | Gets and sets the data type that can be processed by this print queue.
get/put_PrintProcessor | Gets and sets the print processor associated with this print queue.
get/put_Description | Gets and sets the description of this print queue.
get/put_Location | Gets and sets the administrator's description of the print queue location.
get/put_StartTime | Gets and sets the time when the print queue starts processing jobs.
get/put_UntilTime | Gets and sets the time at which the print queue stops processing jobs.
get/put_DefaultJobPriority | Gets and sets the default priority assigned to each print job.
get/put_Priority | Gets and sets the priority of this printer object's job queue for connected devices.
get/put_BannerPage | Gets and sets the file system path to the banner page file used to separate jobs.
get/put_PrintDevices | Gets and sets the names of print devices that this print queue uses as spooling devices.
get/put_NetAddresses | Gets and sets the binding information.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPrintQueue Property Methods

The property methods of the IADsPrintQueue interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
BannerPage

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_BannerPage
([out] BSTR *pbstrBannerPage);

HRESULT put_BannerPage
([in] BSTR bstrBannerPage);

 The file system path that points to the banner page used to separate print jobs. If NULL, no banner page is used.
Datatype

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Datatype
([out] BSTR *pbstrDatatype);

HRESULT put_Datatype
([in] BSTR bstrDatatype);

 The data type that can be processed by this queue.
DefaultJobPriority

[Visual Basic]
Access: Read/Write
Data Type: LONG/BSTR

[C++]
HRESULT get_DefaultJobPriority
([out] LONG *plDefaultJobPriority);

HRESULT put_DefaultJobPriority
([in] BSTR lDefaultJobPriority);

 The default priority assigned to each print job.
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The text description of this print queue.
HostComputer

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HostComputer
([out] BSTR *pbstrHostComputer);

HRESULT put_HostComputer
([in] BSTR bstrHostComputer);

 The ADsPath string that references the host computer.
Location

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Location
([out] BSTR *pbstrLocation);

HRESULT put_Location
([in] BSTR bstrLocation);

 The location of the queue as described by an administrator.
Model

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Model
([out] BSTR *pbstrModel);

HRESULT put_Model
([in] BSTR bstrModel);

 The name of the driver used by this print queue.
PrintDevices

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_PrintDevices
([out] VARIANT *pvPrintDevices);

HRESULT put_PrintDevices
([in] VARIANT vPrintDevices);

 A SAFEARRAY of BSTR that contains the names of the print devices to which this print queue spools jobs.
PrinterPath

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PrinterPath
([out] BSTR *pbstrPrinterPath);

HRESULT put_PrinterPath
([in] BSTR bstrPrinterPath);

 The string that references the path by which a shared printer can be accessed.
PrintProcessor

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_PrintProcessor
([out] BSTR *pbstrPrintProcessor);

HRESULT put_PrintProcessor
([in] BSTR bstrPrintProcessor);

 The print processor associated with this queue.
Priority

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Priority
([out] LONG *plPriority);

HRESULT put_Priority
([in] LONG lPriority);

 The priority of this printer object's job queue for any connected devices. All jobs in higher priority print queue objects will be processed before those in lower priority print queue objects.
StartTime

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_StartTime
([out] DATE *pdateStartTime);

HRESULT put_StartTime
([in] DATE dateStartTime);

 The time when the queue should begin processing jobs. The date portion of the time is ignored.
UntilTime

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_UntilTime
([out] DATE *pdateUntilTime);

HRESULT put_UntilTime
([in] DATE dateUntilTime);

 The time when the queue should stop processing jobs.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to determine if a specified printer is online or offline.

Dim pq as IADsPrintQueue
Dim pqo as IADsPrintQueueOperations

Set pq = GetObject("WinNT://aMachine/aPrinter")
Set pqo = pq
If pqo.status = ADS_PRINTER_OFFLINE Then
    MsgBox pq.Model & "@" & pq.Location & is offline."
Else
    MsgBox pq.Model & "@" & pq.Location & is online."
End If

Example Code [C++]

The following C++ code snippet shows how to determine if a specified printer is online or offline.

IADsPrintQueue *pq;
HRESULT hr;
LPWSTR adsPath = L"WinNT://aMachine/aPrinter";
hr = ADsGetObject(adsPath,
                  IID_IADsPrintQueue,
                  (void**)&pq);
if(FAILED(hr)) exit(hr);

IADsPrintQueueOperations *pqo;
hr = pq->QueryInterface(IID_IADsPrintQueueOperations,(void**)&pqo);
if(FAILED(hr)) exit(hr);

long status;
hr = pqo->get_Status(&status);
if(FAILED(hr)) exit(hr);

BSTR model,location;
hr = pq->get_Model(&model);
if(FAILED(hr)) exit(hr);

hr =pq->get_Location(&location);
if(FAILED(hr)) exit(hr);

if(status == ADS_PRINTER_OFFLINE) {
    printf("%S @ %S is offline.\n",model,location);
}
else {
    printf("%S @ %S is online.\n",model,location);
}
SysFreeString(model);
SysFreeString(location);
pq->Release();
pqo->Release();


IADsService

The IADsService interface is designed for maintaining the information about system services running on a host computer. Examples of such services include "FAX" for Microsoft Fax Service, "RemoteAccess" for Routing and RemoteAccess Service, and "seclogon" for Secondary Logon Service. Examples of the information about any system service include the path to the executable file on the host computer, the type of the service, other services or load group required to run a particular service, and others. IADsService exposes several properties to represent such information.

To find out services available on a host computer, you must first bind to the computer and then enumerate through the service available on that machine. This procedure is shown in the following code fragment:

Dim comp as IADsComputer
Set comp = GetObject("WinNT://aComputer,Computer")
comp.Filter = Array("Service")
For Each srvc in comp
    MsgBox srvc.Name & ", " & srvc.status
Next

The system services are published in the underlying directory. Some may be running, others may not. To check the status or to operate on any of the services, use the properties and methods of the IADsServiceOperations interface.

File service is a special case of the system service. The IADsFileService and IADsFileServiceOperations interfaces support additional functionality unique to file services.

IADsService is a dual interface that inherits from IADs. The IADsService interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsService property methods | Description
get/put_HostComputer | Gets and sets the host of this service.
get/put_DisplayName | Gets and sets the display name of this service.
get/put_Version | Gets and sets the version information of this service.
get/put_ServiceType | Gets and sets the process type in which this service runs.
get/put_StartType | Gets and sets the type for how this service starts.
get/put_Path | Gets and sets the path and filename of the executable.
get/put_StartupParameters | Gets and sets the parameters passed at start-up.
get/put_ErrorControl | Gets and sets the actions taken in case of service failure.
get/put_LoadOrderGroup | Gets and sets the load order group for this service.
get/put_ServiceAccountName | Gets and sets the authentication account name.
get/put_ServiceAccountPath | Gets and sets the path to user object to be authenticated.
get/put_Dependencies | Gets and sets the array of BSTR names of services or load groups that must be loaded in order for this service to load.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsService Property Methods

The property methods of the IADsService interface read and write the properties described in this topic. For more information see Interface Property Methods.

Properties in Vtable Order

Property Description
Dependencies

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Dependencies
([out] VARIANT *pvServiceDepend);

HRESULT put_Dependencies
([in] VARIANT vServiceDepend);

 Array of BSTR names of services or load groups that must be loaded in order for this service to load. The syntax for the entry is "Service:" followed by the service name or "Group:" followed by the load group name.
DisplayName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_DisplayName
([out] BSTR *pbstrDisplayName);

HRESULT put_DisplayName
([in] BSTR bstrDisplayName);

 The friendly display name of the service.
ErrorControl

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ErrorControl
([out] LONG *plErrorControl);

HRESULT put_ErrorControl
([in] LONG lErrorControl);

 Action that should be taken if this service fails on startup. Valid values are:
ADS_SERVICE_ERROR_IGNORE
ADS_SERVICE_ERROR_NORMAL
ADS_SERVICE_ERROR_SEVERE
ADS_SERVICE_ERROR_CRITICAL 		0x00000000
0x00000001
0x00000002
0x00000003
HostComputer

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HostComputer
([out] BSTR *pbstrHostComputer);

HRESULT put_HostComputer
([in] BSTR bstrHostComputer);

 The ADsPath string of the host of this service.
LoadOrderGroup

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LoadOrderGroup
([out] BSTR *pbstrLoadOrderGroup);

HRESULT put_LoadOrderGroup
([in] BSTR bstrLoadOrderGroup);

 Name of the load order group that this service is a member of.
Path

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Path
([out] BSTR *pbstrPath);

HRESULT put_Path
([in] BSTR bstrPath);

 Path and filename to the executable of this service.
ServiceAccountName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ServiceAccountName
([out] BSTR *pbstrServiceAccountName);

HRESULT put_ServiceAccountName
([in] BSTR bstrServiceAccountName);

 Name of the account that this service uses to authenticate itself on startup.
ServiceAccountPath

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ServiceAccountPath
([out] BSTR *pbstrServiceAccountPath);

HRESULT put_ServiceAccountPath
([in] BSTR bstrServiceAccountPath);

 Path of the account specified by the ServiceAccountName property.
ServiceType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ServiceType
([out] LONG *plServiceType);

HRESULT put_ServiceType
([in] LONG lServiceType);

 The description of how a service manifests itself on the host computer. ServiceType is a bit mask that can contain the logical OR of the supported service type constant values. Valid values are as follows:
ADS_SERVICE_KERNEL_DRIVER
ADS_SERVICE_FILE_SYSTEM_DRIVER
ADS_SERVICE_OWN_PROCESS
ADS_SERVICE_SHARE_PROCESS 		0x00000001
0x00000002
0x00000010
0x00000020
StartType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_StartType
([out] LONG *plStartType);

HRESULT put_StartType
([in] LONG lStartType);

 The type you should use to start this service. Valid types are defined as follows and are derived from the appropriate Win32 SERVICE_XXX constants.
ADSI Service Start Type
ADS_SERVICE_BOOT_START
ADS_SERVICE_SYSTEM_START
ADS_SERVICE_AUTO_START
ADS_SERVICE_DEMAND_START
ADS_SERVICE_DISABLED 		Win32 Service Start Type
ADS_SERVICE_BOOT_START
ADS_SERVICE_SYSTEM_START
ADS_SERVICE_AUTO_START
ADS_SERVICE_DEMAND_START
ADS_SERVICE_DISABLED
StartupParameters

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_StartupParameters
([out] BSTR *pbstrStartupParameters);

HRESULT put_StartupParameters
([in] BSTR bstrStartupParameters);

 Parameters passed to the service on startup.
Version

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Version
([out] BSTR *pbstrVersion);

HRESULT put_Version
([in] BSTR bstrVersion);

 Version information of the service.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to list all the available system services running on the host machine, "myMachine", together with the location to find the executables of the services.

Dim cp as IADsComputer
Set cp = GetObject("WinNT://myMachine,computer")
If (IsEmpty(cp) = False) then
    cp.Filter = Array("Service")
    For Each service in cp
        MsgBox service.Name & " @" & service.path
    Next
End if

See Also

IADsFileService, IADsFileServiceOperations, IADsServiceOperations, IADsPrintQueueOperations


IADsUser

The IADsUser interface is designed for representing and managing an end-user account on a network. You can call the methods on this interface to access and manipulate the information about an end-user account. Such information includes names of the user, telephone numbers, job title, and so on. This interface also supports the functionality for determining the group association of the user, and for setting or changing the password.

To bind to a domain user through a WinNT provider, use the domain name as part of the ADsPath, as shown in the following line of code.

GetObject("WinNT://MYDOMAIN/jsmith,user")

Similarly, use the computer name as part of the AdsPath to bind to a local user:

GetObject("WinNT://MYCOMPUTER/jsmith,user")

In Active Directory™, domain users reside in the directory. The following line of code shows how to bind to a domain user through an LDAP provider:

GetObject("LDAP://CN=John Smith,OU=Sales,DC=ArcadiaBay,DC=Com")

However, local accounts reside in the local SAM database and the LDAP provider does not communicate with the local database. Thus, to bind to a local user, you must go through a WinNT provider as described in the second code fragment.

IADsUser is a dual interface that inherits from IADs. The IADsUser interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsUser properties and methods | Description
Get_BadLoginAddress | Gets the address of the last node, considered an "Intruder".
Get_BadLoginCount | Gets the number of the bad logon attempts since last reset.
Get_LastLogin | Gets the date and time of the last network login.
Get_LastLogoff | Gets the date and time of the last network logoff.
Get_LastFailedLogin | Gets the date and time of the last failed network login.
Get_PasswordLastChanged | Gets the date and time of the last password change.
Get/put_Description | Gets and sets the description of the user account.
Get/put_Division | Gets and sets the division within a company (organization).
Get/put_Department | Gets and sets the organizational unit within the organization.
Get/put_EmployeeID | Gets and sets employee identification number of the user.
Get/put_FullName | Gets and sets the full name of the user.
Get/put_FirstName | Gets and sets the first name of the user.
Get/put_LastName | Gets and sets the last name of the user.
Get/put_OtherName | Gets and sets the additional name, such as the nickname, or the middle name of the user.
Get/put_NamePrefix | Gets and sets the name prefix, such as Mr. Ms., or Hon., of the user
Get/put_NameSuffix | Gets and sets the name suffix (Jr., III) of the user.
Get/put_Title | Gets and sets the user's title within the organization.
Get/put_Manager | Gets and sets the manager of the the user.
Get/put_TelephoneHome | Gets and sets the list of home phone numbers of the user. In Active Directory the list has a single element.
Get/put_TelephoneMobile | Gets and sets the list of mobile phone numbers of the user. In Active Directory the list has a single element.
Get/put_TelephoneNumber | Gets and sets the list of work-related phone numbers. In Active Directory the list has a single element.
Get/put_TelephonePager | Gets and sets the list of pager phone numbers.
Get/put_FaxNumber | Gets and sets the list of fax phone numbers. In Active Directory the list has a single element.
Get/put_OfficeLocations | Gets and sets the array of end-user locations. In Active Directory the array has a single element.
Get/put_PostalAddresses | Gets and sets the array of end-user post office addresses.
Get/put_PostalCodes | Gets and sets the array of zip codes for the Postal Addresses. In Active Directory the array has a single element.
Get/put_SeeAlso | Gets and sets the array of ADsPaths of other objects related to this user.
Get/put_AccountDisabled | Gets and sets the flag to indicate whether or not the account is disabled.
Get/put_AccountExpirationDate | Gets and sets the expiration date and time of the user.
Get/put_GraceLoginsAllowed | Gets and sets the number of times user can log on after password has expired.
Get/put_GraceLoginsRemaining | Gets and sets the number of grace logins left before locking account.
Get/put_IsAccountLocked | Gets and sets a flag to indicate whether or not an account is locked.
Get/put_LoginHours | Gets and sets the time periods during each day of week indicating valid login periods.
Get/put_LoginWorkstations | Gets and set the workstations and their net addresses for this end-user.
Get/put_MaxLogins | Gets and sets the maximum number of simultaneous logins.
Get/put_MaxStorage | Gets and set the maximum amount of disk space allowed for the user.
Get/put_PasswordExpirationDate | Gets and sets the date and time when password will expire.
Get/put_PasswordMinimumLength | Gets and sets the minimum number of characters allowed in a password.
Get/put_PasswordRequired | Gets and sets a flag to indicate whether or not a password is required.
Get/put_RequireUniquePassword | Gets and sets a flag to indicate whether or not a new password must be different from ones in the password history list.
Get/put_EmailAddress | Gets and sets the email address of the user.
Get/put_HomeDirectory | Gets and sets the home directory of the user.
Get/put_Languages | Gets and sets the array of language names for the end-user.
Get/put_Profile | Gets and sets the end-user's profile path.
Get/put_LoginScript | Gets and sets the end-user's login script path.
Get/put_Picture | Gets and sets the picture of the user.
Get/put_HomePage | Gets and sets the URL to the home page of the user.
Groups | Determines the groups to which this end-user belongs.
SetPassword | Sets the password.
ChangePassword | Changes password from the specified existing value to a new value.

Remarks

As with any other ADSI object, the container object creates a Windows NT®/Windows® 2000 user account object. This means you need to bind to a container object first. Then call the IADsContainer::Create method and specify all mandatory or optional attributes.

User Creation Using WinNT

With WinNT, you do not have to specify any additional attributes when creating a user. You may call the IADsContainer::Create method to create the user object directly.

Set dom = GetObject("WinNT://aDomain")
Set usr = dom.Create("user","jsmith")
usr.SetInfo

In this case, a domain user is created with the following default attribute values:

Property | Value
Full Name | SAM Account Name (such as jsmith)
Password | Empty
User Must Change Password | TRUE
User Cannot Change Password | FALSE
Password Never Expires | FALSE
Account Disabled | FALSE
Group | Domain User
Profile | Empty
Account Never Expires | TRUE

To create a local user, bind to a target machine, as shown in the following code snippet:

Set mach = GetObject("WinNT://aMachine,Computer")
Set usr = mach.Create("user","jsmith")
usr.SetInfo

The newly created local user will have the same default properties as the domain user discussed earlier. The group membership, however, will be "users", instead of "domain user".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsUser Property Methods

The property methods of the IADsUser interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
AccountDisabled

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_AccountDisabled
([out] BOOLEAN *pbAccountDisabled);

HRESULT put_AccountDisabled
([in] BOOLEAN bAccountDisabled);

 A flag to indicate if the account is or should be disabled.
AccountExpirationDate

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_AccountExpirationDate
([out] DATE *pdateAccountExpirationDate);

HRESULT put_AccountExpirationDate
([in] DATE dateAccountExpirationDate);

 The date and time after which the user cannot log in.
BadLoginAddress

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_BadLoginAddress
([out] BSTR *pbstrBadLoginAddress);

 The last node that is considered a possible intruder (available if Intruder detection is active).
BadLoginCount

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_BadLoginCount
([out] LONG *plBadLoginCount);

 The number of bad logon attempts since the last reset.
Department

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Department
([out] BSTR *pbstrDepartment);

HRESULT put_Department
([in] BSTR bstrDepartment);

 The department, an organizational unit (OU), within the company to which the user belongs.
Description

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Description
([out] BSTR *pbstrDescription);

HRESULT put_Description
([in] BSTR bstrDescription);

 The text description of the user.
Division

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Division
([out] BSTR *pbstrDivision);

HRESULT put_Division
([in] BSTR bstrDivision);

 The division within a company (organization).
EmailAddress

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_EmailAddress
([out] BSTR *pbstrEmailAddress);

HRESULT put_EmailAddress
([in] BSTR bstrEmailAddress);

 The user's e-mail address.
EmployeeID

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_EmployeeID
([out] BSTR *pbstrEmployeeID);

HRESULT put_EmployeeID
([in] BSTR bstrEmployeeID);

 The employee identifier of the user.
FaxNumber

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_FaxNumber
([out] VARIANT *pvarFaxNumber);

HRESULT put_FaxNumber
([in] VARIANT varFaxNumber);

 The fax numbers of the user. In Active Directory, this property is single valued and the VARIANT array has one element.
FirstName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_FirstName
([out] BSTR *pbstrFirstName);

HRESULT put_FirstName
([in] BSTR bstrFirstName);

 The first name of the user.
FullName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_FullName
([out] BSTR *pbstrFullName);

HRESULT put_FullName
([in] BSTR bstrFullName);

 The full name of the user.
GraceLoginsAllowed

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_GraceLoginsAllowed
([out] LONG *plGraceLoginsAllowed);

HRESULT put_GraceLoginsAllowed
([in] LONG lGraceLoginsAllowed);

 The number of times the user can log on after the password has expired.
GraceLoginsRemaining

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_GraceLoginsRemaining
([out] LONG *plGraceLoginsRemaining);

HRESULT put_GraceLoginsRemaining
([in] LONG lGraceLoginsRemaining);

 The number of grace logons left before the account is locked.
HomeDirectory

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HomeDirectory
([out] BSTR *pbstrHomeDirectory);

HRESULT put_HomeDirectory
([in] BSTR bstrHomeDirectory);

 The user's home directory.
HomePage

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_HomePage
([out] BSTR *pbstrHomePage);

HRESULT put_HomePage
([in] BSTR bstrHomePage);

 The Uniform Resource Locator (URL) for the user's home page.
IsAccountLocked

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_IsAccountLocked
([out] BOOLEAN *pbIsAccountLocked);

HRESULT put_IsAccountLocked
([in] BOOLEAN bIsAccountLocked);

 A flag indicating if the account is locked because of intruder detection.
Languages

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Languages
([out] VARIANT *pvLanguages);

HRESULT put_Languages
([in] VARIANT vLanguages);

 An array of BSTR language names for the user.
LastFailedLogin

[Visual Basic]
Access: Read
Data Type: DATE

[C++]
HRESULT get_LastFailedLogin
([out] DATE *pdateLastFailedLogin);

 The date and time of the last failed network login.
LastLogin

[Visual Basic]
Access: Read
Data Type: DATE

[C++]
HRESULT get_LastLogin
([out] DATE *pdateLastLogin);

 The date and time of the last network login.
LastLogoff

[Visual Basic]
Access: Read
Data Type: DATE

[C++]
HRESULT get_LastLogoff
([out] DATE *pdateLastLogoff);

 The date and time of the last network logoff.
LastName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LastName
([out] BSTR *pbstrLastName);

HRESULT put_LastName
([in] BSTR bstrLastName);

 The user's last name.
LoginHours

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_LoginHours
([out] VARIANT *pvLoginHours);

HRESULT put_LoginHours
([in] VARIANT vLoginHours);

 Time periods for each day of the week during which logons are permitted for the user. Represented as a table of Booleans for the week, each indicating if that time slot is a valid logon time.
LoginScript

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_LoginScript
([out] BSTR *pbstrLoginScript);

HRESULT put_LoginScript
([in] BSTR bstrLoginScript);

 The logon script path.
LoginWorkstations

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_LoginWorkstations
([out] VARIANT *pvLoginWorkstations);

HRESULT put_LoginWorkstations
([in] VARIANT vLoginWorkstations);

 Addresses or names of workstations, of the BSTR data type, from which the user can log on.
Manager

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Manager
([out] BSTR *pbstrManager);

HRESULT put_Manager
([in] BSTR bstrManager);

 The manager of the user.
MaxLogins

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MaxLogins
([out] LONG *plMaxLogins);

HRESULT put_MaxLogins
([in] LONG lMaxLogins);

 The number of simultaneous login sessions allowed.
MaxStorage

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_MaxStorage
([out] LONG *plMaxStorage);

HRESULT put_MaxStorage
([in] LONG lMaxStorage);

 The maximum amount of disk space, in kilobytes, that the user can have.
NamePrefix

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_NamePrefix
([out] BSTR *pbstrNamePrefix);

HRESULT put_NamePrefix
([in] BSTR bstrNamePrefix);

 Name prefix of the user, for example "Ms.", or "Hon."
NameSuffix

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_NameSuffix
([out] BSTR *pbstrNameSuffix);

HRESULT put_NameSuffix
([in] BSTR bstrNameSuffix);

 Name suffix of the user, for example "Jr.", or "III".
OfficeLocations

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_OfficeLocations
([out] VARIANT *pvOfficeLocations);

HRESULT put_OfficeLocations
([in] VARIANT vOfficeLocations);

 Office location as a BSTR array for the user. For Active Directory, this property is single-valued and the array has one element.
OtherName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_OtherName
([out] BSTR *pbstrOtherName);

HRESULT put_OtherName
([in] BSTR bstrOtherName);

 An additional name, for example, the middle name, for the user.
PasswordExpirationDate

[Visual Basic]
Access: Read/Write
Data Type: DATE

[C++]
HRESULT get_PasswordExpirationDate
([out] DATE *pdatePasswordExpirationDate);

HRESULT put_PasswordExpirationDate
([in] DATE datePasswordExpirationDate);

 The date and time when the password expires.
PasswordLastChanged

[Visual Basic]
Access: Read
Data Type: DATE

[C++]
HRESULT get_PasswordLastChanged
([out] DATE *pdatePasswordLastChanged);

 The last time the password was changed.
PasswordMinimumLength

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_PasswordMinimumLength
([out] LONG *plPasswordMinimumLength);

HRESULT put_PasswordMinimumLength
([in] LONG lPasswordMinimumLength);

 The minimum password length.
PasswordRequired

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_PasswordRequired
([out] BOOLEAN *plPasswordRequired);

HRESULT put_PasswordRequired
([in] BOOLEAN lPasswordRequired);

 A flag indicating if the password is required.
Picture

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Picture
([out] VARIANT *pvarPicture);

HRESULT put_Picture
([in] VARIANT varPicture);

 An OctetString array of bytes that hold an image.
PostalAddresses

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_PostalAddresses
([out] VARIANT *pvPostalAddresses);

HRESULT put_PostalAddresses
([in] VARIANT vPostalAddresses);

 Postal address as a BSTR array. This property is multi-valued to hold more than addresses of the user. The internal format of a PostalAddress should comply with CCITT F.401 as referenced in X.521-1993, which defines a PostalAddress as six elements of 30 bytes each, holding a street address, (optionally) Post Office Box, city or locality, state or province, Postal Code, and Country.
PostalCodes

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_PostalCodes
([out] VARIANT *pvPostalCodes);

HRESULT put_PostalCodes
([in] VARIANT vPostalCodes);

 Postal codes as a BSTR array. Postal codes are positionally linked to the PostalAddresses array. In Active Directory, however, this property is single-valued and the array has a single element.
Profile

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Profile
([out] BSTR *pbstrProfile);

HRESULT put_Profile
([in] BSTR bstrProfile);

 The path to the user profile.
RequireUniquePassword

[Visual Basic]
Access: Read/Write
Data Type: BOOLEAN

[C++]
HRESULT get_RequireUniquePassword
([out] BOOLEAN *plRequireUniquePassword);

HRESULT put_RequireUniquePassword
([in] BOOLEAN lRequireUniquePassword);

 A flag indicating if a new password should be different from those known through a password history.
SeeAlso

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_SeeAlso
([out] VARIANT *pvSeeAlso);

HRESULT put_SeeAlso
([in] VARIANT vSeeAlso);

 An array of ADsPaths of other objects related to the user.
TelephoneHome

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_TelephoneHome
([out] VARIANT *pvarTelephoneHome);

HRESULT put_TelephoneHome
([in] VARIANT varTelephoneHome);

 An array of home telephone numbers of the user. In Active Directory, this property is single-valued and the array has one element.
TelephoneMobile

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_TelephoneMobile
([out] VARIANT *pvarTelephoneMobile);

HRESULT put_TelephoneMobile
([in] VARIANT varTelephoneMobile);

 An array of mobile telephone numbers of the user. In Active Directory this property is single-valued and the array has one element only.
TelephoneNumber

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_TelephoneNumber
([out] VARIANT *pvarTelephoneNumber);

HRESULT put_TelephoneNumber
([in] VARIANT varTelephoneNumber);

 An array of (usually work-related) telephone numbers associated with the user. In Active Directory this property is single-valued and the array is of a single element.
TelephonePager

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_TelephonePager
([out] VARIANT *pvarTelephonePager);

HRESULT put_TelephonePager
([in] VARIANT varTelephonePager);

 An array of pager numbers of the user. In Active Directory this property is single-valued and the array is of a single element.
Title

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Title
([out] BSTR *pbstrTitle);

HRESULT put_Title
([in] BSTR bstrTitle);

 The title of the user.

Remarks

The WinNT provider supplied by Microsoft does not support all the IADsUser property methods as presented above. However, the provider supports other properties that can be accessed using the IADs::Get or IADs::Put method. For the list of unsupported properties, code examples, and other information, see WinNT User Object in ADSI WinNT Provider.

The ADSI LDAP provider specific features of the user class object is discussed in LDAP User Object in ADSI LDAP Provider. The topic includes Mapping between IADsUser Properties and Active Directory Properties, as well as code examples for managing user account.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to bind to a user account object and retrieve the user's full name.

Dim usr As IADsUser
Set usr = GetObject("WinNT://ArcadiaBay/JSmith,user")
fulNamb = usr.FullName

Example Code [C++]

The following C++/C code snippet shows how to bind to a user account object and retrieve the user's full name.

IADsUser *GetUserObject(LPWSTR uPath)
{
    IADsUser *pUser;
    HRESULT hr = ADsGetObject(uPath,IID_IADsUser,(void**)&pUser);
    if (FAILED(hr)) {return NULL;}
    BSTR bstr;
    hr = pUser->get_FullName(&bstr);
    printf("User: %S\n", bstr);
    SysFreeString(bstr);
    return pUser;
}

See Also

WinNT User Object, LDAP User Object


IADsUser::ChangePassword

The IADsUser::ChangePassword method changes the user password from the specified old value to a new value.

HRESULT IADsUser::ChangePassword(
  BSTR bstrOldPassword,
  BSTR bstrNewPassword
);

Parameters

bstrOldPassword
[in] The password as it currently exists.
bstrNewPassword
[out] The new password to use.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to change the password of a user.

Dim usr As IADsUser
Set usr = GetObject("WinNT://ArcadiaBay/JSmith,user")
usr.ChangePassword "guesswhat?", "topsecret98"

Example Code [C++]

The following C++ code snippet shows how to change the password of a user.

HRESULT ChangePassword(
    IADsUser *pUser,
    LPWSTR oldPasswd,
    LPWSTR newPasswd)
{
    HRESULT hr=S_OK;
    if(!pUser) { return E_FAIL;}
    hr = pUser->ChangePassword(oldPasswd, newPasswd);
    printf("User password has been changed from %S to %S\n",
                          oldPasswd, newPasswd);
     return hr;
}

HRESULT GetUserObject(LPWSTR); // function listed elsewhere in
                               // IADsUser Property Methods.

int main(int argc, char* argv[])
{
    HRESULT hr = CoInitialize(NULL);
    IADsUser *pUser = GetUserObject(
                      L"WinNT://ArcadiaBay/JSmith,user");
    pUser-AddRef();
    hr = ChangePassword(pUser, L"guessWhat", L"tOpSecrete98");
    if(pUser) pUser->Release();
    CoUninitialize();
    return 0;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsUser::Groups

The IADsUser::Groups method obtains a collection of the ADSI group objects to which this user belongs. The method returns an IADsMembers interface pointer through which you can enumerate all the groups in the collection.

HRESULT IADsUser::Groups(
  IADsMembers ** ppGroups
);

Parameters

ppGroups
[out] Indirect pointer to the IADsMembers interface on a members object that can be enumerated using IEnumVARIANT to determine the groups to which this end-user belongs.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet examines the group membership a user belongs to.

Dim usr As IADsUser
Set usr = GetObject("WinNT://ArcadiaBay/JSmith,user")
For Each grp In usr.Groups
    Debug.Print  grp.Name & " (" & grp.Class & ")"
Next

Example Code [C++]

The following C++ code snippet examines the group membership a user belongs to.

HRESULT CheckUserGroups(IADsUser *pUser)
{
    IADsMembers *pGroups;
    HRESULT hr = S_OK;
    hr = pUser->Groups(&pGroups);
    pUser->Release();
    if (FAILED(hr)) return hr;

    IUnknown *pUnk;
    hr = pGroups->get__NewEnum(&pUnk);
    if (FAILED(hr)) return hr;
    pGroups->Release();

    IEnumVARIANT *pEnum;
    hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
    if (FAILED(hr)) return hr;

    pUnk->Release();

    // Now Enumerate
    BSTR bstr;
    VARIANT var;
    IADs *pADs;
    ULONG lFetch;
    IDispatch *pDisp;

    VariantInit(&var);
    hr = pEnum->Next(1, &var, &lFetch);
    while(hr == S_OK)
    {
        if (lFetch == 1)
        {
             pDisp = V_DISPATCH(&var);
             pDisp->QueryInterface(IID_IADs, (void**)&pADs);
             pADs->get_Name(&bstr);
             printf("Group belonged: %S\n",bstr);
             SysFreeString(bstr);
             pADs->Release();
        }
        VariantClear(&var);
        pDisp=NULL;
        hr = pEnum->Next(1, &var, &lFetch);
    };
    hr = pEnum->Release();
    return S_OK;
}

HRESULT GetUserObject(LPWSTR); // function listed elsewhere in
                               // IADsUser Property Methods.

int main(int argc, char* argv[])
{
    HRESULT hr = CoInitialize(NULL);
    IADsUser *pUser = GetUserObject(
                      L"WinNT://ArcadiaBay/JSmith,user");
    pUser->AddRef();
    hr = CheckUserGroups(pUser);
    if(pUser) pUser->Release();
    CoUninitialize();
    return 0;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsUser::SetPassword

The IADsUser::SetPassword method sets the user password to a specified value. The user account must have been created and stored in the underlying directory using IADs::SetInfo before IADsUser::SetPassword is called.

HRESULT IADsUser::SetPassword(
  BSTR bstrNewPassword
);

Parameters

bstrNewPassword
[in] The new password value.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to set the user password, assuming you have the permission to do so.

Dim usr As IADsUser
Set usr = GetObject("WinNT://ArcadiaBay/JSmith,user")
usr.SetPassword "topsecret98"

Example Code [C++]

The following C++ code snippet shows how to set the user password, assuming you have the permission.

HRESULT SetPassword(IADsUser *pUser, LPWSTR passwd)
{
    HRESULT hr=S_OK;
    if(!pUser) { return E_FAIL;}
    hr = pUser->SetPassword(passwd);
    if (hr == S_OK) printf("User password has been setn");
    pUser->Release();
    return hr;
}

HRESULT GetUserObject(LPWSTR); // function listed elsewhere in
                               // IADsUser Property Methods.

int main(int argc, char* argv[])
{
    HRESULT hr = CoInitialize(NULL);
    IADsUser *pUser = GetUserObject(
                      L"WinNT://ArcadiaBay/JSmith,user");
    pUser->AddRef();
    hr = SetPassword(pUser, L"tOpSecrete98");
    if(pUser) pUser->Release();
    CoUninitialize();
    return 0;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsServiceOperations


Dynamic Object Interfaces

This section describes the following dynamic object interfaces:

  • IADsComputerOperations
  • IADsFileServiceOperations
  • IADsPrintJobOperations
  • IADsPrintQueueOperations
  • IADsResource
  • IADsServiceOperations
  • IADsSession


IADsComputerOperations

The IADsComputerOperations interface exposes methods for retrieving the status of a computer over a network and to enable remote shut-down. Directory service providers may choose to implement this interface to support basic system administrations over a network through ADSI.

IADsComputerOperations is dual interface that inherits from IADs. The IADsComputerOperations interface exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsComputerOperations methods | Description
Status | Returns the current operations status
Shutdown | Executes shutdown with optional reboot.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsComputer


IADsComputerOperations::Shutdown

The IADsComputerOperations::Shutdown method causes a computer under ADSI control to execute the shutdown operation with an optional reboot.

HRESULT IADsComputerOperations::Shutdown(
  boolean bReboot
);

Parameters

bReboot
[in] If TRUE then reboot the computer after the shutdown is complete.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The shutdown command has been issued successfully.
E_FAIL
The request is denied.

For other return values, see ADSI Error Codes.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsComputer


IADsComputerOperations::Status

The IADsComputerOperations::Status method retrieves the status of a computer.

HRESULT IADsComputerOperations::Status(
  IDispatch ** ppObject
);

Parameters

ppObject
[out, retval] Pointer to an IDispatch interface that reports the status code of computer operations. The status code is provider-specific.

Return Values

This method supports the standard return values as well as:

S_OK
The object containing the status code is obtained successfully.
E_FAIL
The operation has failed.

For other return values, see ADSI Error Codes.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsComputer


IADsFileServiceOperations

The IADsFileServiceOperations interface extends the functionality, as exposed in the IADsServiceOperations interface, for managing the file service across a network. Specifically, it serves to maintain and manage open resources and active sessions of the file service.

To bind to a file service operations object, you must use the ADsPath string identifying the "LanmanServer" service on the host computer, as shown in the following code snippet:

Dim fso As IADsFileServiceOperations
Set fso = GetObject("WinNT://aServer/aComputer/LanmanServer")

From this point on, you can treat the file service object as just a service object, applying any of the methods of IADsServiceOperations to the file service object. For example, you can examine the operational status of the file service, start or stop the file service, or change its password.

However, the IADsFileServiceOperations interface allows you to work with open resources and active sessions of the file service. For example,

For Each r in fso.Resources
   MsgBox r.User
   MsgBox r.Path
   MsgBox r.LockCount
Next

For more information on active sessions and open resources, see IADsSession and IADsResource.

IADsFileServiceOperations is dual interface that inherits from IADsServiceOperations. The IADsFileServiceOperations interface exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsFileServiceOperations methods | Description
Sessions | Gets an interface pointer on a collection object that represents current open sessions on this file service.
Resources | Gets an interface pointer on a collection object that represents current open resources for this file service.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsFileService, IADsServiceOperations, IADsService


IADsFileServiceOperations::Resources

The IADsFileServiceOperations::Resources method gets an indirect pointer to the IADsCollection interface on a collection of the resource objects representing the current open resources on this file service.

HRESULT IADsFileServiceOperations::Resources(
  IADsCollection ** ppResources
);

Parameters

ppResources
[out] Indirect pointer to the IADsCollection interface that can then be used to enumerate objects implementing the IADsResource interface and representing the current open resources for this file service.

Return Values

This method supports the standard return values including S_OK. For other return values, see ADSI Error Codes.

Remarks

Traditional directory services supply information only about directory service elements that are represented in the underlying data store. Information about resources for file services may not be available from the underlying directory store.

Example Code [Visual Basic]

The following Visual Basic® code snippet shows how to enumerate open resources managed by a file service.

Dim fso as IADsFileServiceOperations

'  bind to a file service operation on "myComputer"
'  in the local domain.
Set fso = GetObject("WinNT://myComputer/LanmanServer")

' enumerates resources
For Each resource In fso.Resources
   MsgBox "Resource path: " & resource.Path
Next resource

Example Code [C++]

For a C++ code snippet using the IADsFileServiceOperations::Resources method, see the code snippet given in IADsResource.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsFileService, IADsResource


IADsFileServiceOperations::Sessions

The IADsFileServiceOperations::Sessions method gets a pointer to the IADsCollection interface on a collection of the session objects representing the current open sessions for this file service.

HRESULT IADsFileServiceOperations::Sessions(
  IADsCollection ** ppSessions
);

Parameters

ppSessions
[out] Indirect pointer to the IADsCollection interface used to enumerate objects that implement the IADsSession interface and represent the current open sessions for this file service.

Return Values

This method supports the standard return values including S_OK. For other return values, see ADSI Error Codes.

Remarks

Traditional directory services supply information only about directory service elements that are represented in the underlying data store. Information about sessions for file services may not be available from the underlying store.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to enumerate active sessions managed by a file service.

Dim fso as IADsFileServiceOperations

'  bind to a file service operation on "myComputer"
'  in the local domain.
Set fso = GetObject("WinNT://myComputer/LanmanServer")

' enumerate sessions
For Each session In fso.sessions
   MsgBox "Session name: " & session.Name
Next session

Example Code [C++]

For a C++ code snippet using the IADsFileServiceOperations::Session interface, see the code snippet given in IADsSession.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsFileService, IADsCollection, IADsSession


IADsPrintJobOperations

The IADsPrintJobOperations interface is used to control a print job across a network. A print job object implementing the IADsPrintJob interface supports this interface to provide the following functionality:

  • To examine the operational status and other information.
  • To interrupt a running print job.
  • To resume a paused print job.

IADsPrintJobOperations is a dual interface that inherits from IADs. The IADsPrintJobOperations interface exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPrintJobOperations properties and methods | Description
get_Status | Status of print job.
get_TimeElapsed | Elapsed time in seconds since job started printing.
get_PagesPrinted | Number of pages completed.
get/put_Position | Numeric position of print job in print queue.
Pause | Pauses processing of this print job.
Resume | Resumes processing of this print job.

Example Code [Visual Basic]

The following Visual Basic code fragment shows how this interface can be used.

Dim pqo As IADsPrintQueueOperations
Dim pjo As IADsPrintJobOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
For each pj in pqo.PrintJobs
    set pjo = pj
    MsgBox "Print job status: " & Hex(pjo.status)
Next

Example Code [C++]

The following C++ code fragment shows how this interface can be used.

IADsPrintJobOperations *pjo;
IADsPrintQueueOperations *pqo;

HRESULT hr ;
hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                  IID_IADsPrintQueueOperations,
                  (void**)&pqo);
if (FAILED(hr)) exit(hr);

IADsCollection *pColl;
hr = pqo->PrintJobs(&pColl);

IUnknown *pUnk = NULL;
hr = pColl->get__NewEnum(&pUnk);
pColl->Release();
if (FAILED(hr) ) exit(hr);

IEnumVARIANT *pEnum;
hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
pUnk->Release();
if (FAILED(hr)) exit(hr);

// Now Enumerate
VARIANT var;
ULONG lFetch;
IDispatch *pDisp;

long status;

VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADsPrintJobOperations,
                              (void**)&pjo);

        pjo->get_Status(&status);
        printf("Job status: %x\n",status);

        pjo->Release();
    }
    VariantClear(&var);
    pDisp=NULL;
    hr = pEnum->Next(1, &var, &lFetch);
};
hr = pEnum->Release();

hr = pqo->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPrintJobOperations Property Methods

The property methods of the IADsPrintJobOperations interface read and write the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Status

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Status
([out] LONG *plStatus);

 One of the following integer values that indicate current status of service:
Status
ADS_JOB_PAUSED
ADS_JOB_ERROR
ADS_JOB_DELETING
ADS_JOB_PRINTING
ADS_JOB_OFFLINE
ADS_JOB_PAPEROUT
ADS_JOB_PRINTED
ADS_JOB_DELETED 		Hex Value
0x00000001
0x00000002
0x00000004
0x00000010
0x00000020
0x00000040
0x00000080
0x00000100
TimeElapsed

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_TimeElapsed
([out] LONG *plTimeElapsed);

 Time elapsed, in seconds, since the print job started.
PagesPrinted

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_PagesPrinted
([out] LONG *plPagesPrinted);

 Number of pages already printed.
Position

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Position
([out] LONG *plPosition);

HRESULT put_Position
([in] LONG lPosition);

 Position in the print queue of this print job.

Code Example [Visual Basic]

The following Visual Basic code fragment shows how the properties for IADsPrintJobOperations may be used:

Dim pqo As IADsPrintQueueOperations
Dim pjo As IADsPrintJobOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
For each pj in pqo.PrintJobs
    set pjo = pj
    MsgBox pjo.PagesPrinted & " pages printed for job " & pj.Name
    if (pjo.position > 1) Then
        pjo.Position = pjo.status - 1
    end if
Next

Example Code [C++]

For a C++ code snippet, see Example Code [C++] given in the introductory section of the IADsPrintJobOperations interface.

See Also

IADsPrintJob, IADsPrintQueue


IADsPrintJobOperations::Pause

The IADsPrintJobOperations::Pause method halts the processing of the current print job. Call the IADsPrintJobOperations::Resume method to continue the processing.

HRESULT IADsPrintJobOperations::Pause(void);

Return Values

This method supports the standard return values. For other return values, see ADSI Error Codes.

Code Example [Visual Basic]

The following Visual Basic code fragment shows how this interface may be used.

Dim pqo As IADsPrintQueueOperations
Dim pjo As IADsPrintJobOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
For each pj in pqo.PrintJobs
    set pjo = pj
    if (Hex(pjo.status) = ADS_JOB_PRINTING) Then
        pjo.Pause
    end if
Next

Example Code [C++]

For a C++ code snippet, see Example Code [C++] given in the introductory section of the IADsPrintJobOperations interface.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPrintJobOperations::Resume


IADsPrintJobOperations::Resume

The IADsPrintJobOperations::Resume method continues the print job that has been halted by the IADsPrintJobOperations::Pause method.

HRESULT IADsPrintJobOperations::Resume(void);

Return Values

This method supports the standard return values. For other return values, see ADSI Error Codes.

Code Example [Visual Basic]

The following Visual Basic code fragment shows how this method may be used:

Dim pqo As IADsPrintQueueOperations
Dim pjo As IADsPrintJobOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
For each pj in pqo.PrintJobs
    set pjo = pj
    if (Hex(pjo.status) = ADS_JOB_PAUSED) Then
        pjo.resume
    end if
Next

Example Code [C++]

For a C++ code snippet, see Example Code [C++] given in the introductory section of the IADsPrintJobOperations interface.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPrintJobOperations::Pause


IADsPrintQueueOperations

The IADsPrintQueueOperations interface is designed for controlling a printer across a network. It supports the following operations:

  1. To retrieve all the print jobs submitted to the print queue.
  2. To suspend the print queue operation.
  3. To resume the print queue operation.
  4. To remove all the print jobs from the print queue.

IADsPrintQueueOperations is dual interface that inherits from IADs. The IADsPrintQueueOperations interface exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsPrintQueueOperations property methods | Description
get_Status | Current status of print queue service.
PrintJobs | Retrieve an interface pointer on a collection object that represents print jobs managed by this print queue.
Pause | Pause print queue processing of print jobs.
Resume | Resume print queue processing of print jobs.
Purge | Purge all jobs from a print queue.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPrintQueueOperations Property Methods

The property methods of the IADsPrintQueueOperations interface read and write the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Status

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Name
([out] LONG *pbstrName);

HRESULT put_Name
([in] LONG bstrName);

 Current status of the print queue operations. The valid status code values are listed in the following table.
Status
ADS_PRINTER_PAUSED
ADS_PRINTER_PENDING_DELETION
ADS_PRINTER_ERROR
ADS_PRINTER_PAPER_JAM
ADS_PRINTER_PAPER_OUT
ADS_PRINTER_MANUAL_FEED
ADS_PRINTER_PAPER_PROBLEM
ADS_PRINTER_OFFLINE
ADS_PRINTER_IO_ACTIVE
ADS_PRINTER_BUSY
ADS_PRINTER_PRINTING
ADS_PRINTER_OUTPUT_BIN_FULL
ADS_PRINTER_NOT_AVAILABLE
ADS_PRINTER_WAITING
ADS_PRINTER_PROCESSING
ADS_PRINTER_INITIALIZING
ADS_PRINTER_WARMING_UP
ADS_PRINTER_TONER_LOW
ADS_PRINTER_NO_TONER
ADS_PRINTER_PAGE_PUNT
ADS_PRINTER_USER_INTERVENTION
ADS_PRINTER_OUT_OF_MEMORY
ADS_PRINTER_DOOR_OPEN
ADS_PRINTER_SERVER_UNKNOWN
ADS_PRINTER_POWER_SAVE 		Hex Value
0x00000001
0x00000002
0x00000003
0x00000004
0x00000005
0x00000006
0x00000007
0x00000008
0x00000100
0x00000200
0x00000400
0x00000800
0x00001000
0x00002000
0x00004000
0x00008000
0x00010000
0x00020000
0x00040000
0x00080000
0x00100000
0x00200000
0x00400000
0x00800000
0x01000000

Code Example [Visual Basic]

The following Visual Basic code snippet checks if a printer is jammed.

Dim pqo As IADsPrintQueueOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
If pqo.Status = ADS_PRINTER_PAPER_JAM Then
    MsgBox "Your printer is jammed."
End If

Example Code [C++]

The following C++ code snippet checks if a printer is jammed.

IADsPrintQueueOperations *pqo;
HRESULT hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                          IID_IADsPrintQueueOperations,
                          (void**)&pqo)
long status;
hr = pqo->get_Status(&status);
if(status = ADS_PRINTER_PAPER_JAM) {
    printf("Your printer is jammed.\n");
}
hr = pqo->Release();

See Also

IADsPrintQueue


IADsPrintQueueOperations::Pause

The IADsPrintQueueOperations::Pause method suspends the processing of print jobs within a print queue service.

HRESULT IADsPrintQueueOperations::Pause(void);

Return Values

This method supports the standard return values. For other return values see the ADSI Error Codes.

Code Example [Visual Basic]

The following Visual Basic code fragment suspends the operation of a printer:

Dim pqo As IADsPrintQueueOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
if pgo.status = ADS_PRINTER_PRINTING then
    pqo.Pause
end if

Example Code [C++]

The following C++ code snippet suspends the operation of a printer.

IADsPrintQueueOperations *pqo;
HRESULT hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                          IID_IADsPrintQueueOperations,
                          (void**)&pqo)
long status;
hr = pqo->get_Status(&status);
if(status = ADS_PRINTER_PRINTING) {
    pqo->Pause();
}
hr = pqo->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPrintQueueOperations::Resume


IADsPrintQueueOperations::PrintJobs

The IADsPrintQueueOperations::PrintJobs method gets an IADsCollection interface pointer on the collection of the print jobs being processed in this print queue. This collection can be enumerated using the standard Automation enumeration methods on IEnumVARIANT. To delete a print job, use the IADsCollection::Remove method on the retrieved interface pointer.

HRESULT IADsPrintQueueOperations::PrintJobs(
  IADsCollection ** ppPrintJobs
);

Parameters

ppPrintJobs
[out] Indirect pointer to the IADsCollection interface on the collection of objects that have been added to this print queue. Objects in the collection implements the IADsPrintJob interface.

Return Values

This method supports the standard return values. For other return values see the ADSI Error Codes.

Code Example [Visual Basic]

The following Visual Basic code fragment retrieves the collection object holding all the print jobs and then walk through each print job in the print queue.

Dim pqo As IADsPrintQueueOperations
Dim pjs as IADsCollection

Set pqo = GetObject("WinNT://aMachine/aPrinter")
Set pjs = pqo.PrintJobs
For each pj in pjs
    MsgBox pj.TotalPages
Next

Example Code [C++]

For a C++ code snippet using the IADsPrintQueueOperations::PrintJobs method, see the code snippet given in IADsPrintJob Property Methods.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

ADsEnumerateNext


IADsPrintQueueOperations::Purge

The IADsPrintQueueOperations::Purge method empties the print queue of all print jobs without processing them.

HRESULT IADsPrintQueueOperations::Purge(void);

Return Values

This method supports the standard return values. For other return values see the ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code fragment removes all the print jobs from a printer.

Dim pqo As IADsPrintQueueOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
pqo.Purge

Example Code [C++]

The following C++ code snippet removes all the print jobs from a printer.

IADsPrintQueueOperations *pqo;
HRESULT hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                          IID_IADsPrintQueueOperations,
                          (void**)&pqo)
hr = pqo->Purge();
hr = pqo->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPrintQueueOperations::Resume

The IADsPrintQueueOperations::Resume method resumes the processing of the print jobs in the print queue that has previously been suspended.

HRESULT IADsPrintQueueOperations::Resume(void);

Return Values

This method supports the standard return values. For other return values see the ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code fragment resumes the operation of a printer.

Dim pqo As IADsPrintQueueOperations
Set pqo = GetObject("WinNT://aMachine/aPrinter")
pqo.Resume

Example Code [C++]

The following C++ code snippet resumes the operation of a printer.

IADsPrintQueueOperations *pqo;
HRESULT hr = ADsGetObject(L"WinNT://aMachine/aPrinter",
                          IID_IADsPrintQueueOperations,
                          (void**)&pqo)

hr = pqo->Resume();
hr = pqo->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsPrintQueueOperations::Pause


IADsResource

The IADsResource interface is designed for managing an open resource for a file service across a network. When a remote user opens a folder or a subfolder on a public share point on the targeted computer, ADSI considers this folder as an open resource and represents it by a resource object implementing this interface. You obtain the collection of resource objects from a file service operations object, as shown in the following:

Dim fso as IADsFileServiceOperations
Dim rs as IADsCollection
Set fso = GetObject("WinNT://myHost/LanmanServer")
Set rs = fso.Resources

IADsResource is dual interface that inherits from IADs. The IADsResource interface exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsResource property methods | Description
get_User | Gets the name of user for the Resource.
get_UserPath | Gets ADsPath of user object for the user who opened the resource.
get_Path | Gets the file system path of opened resource.
get_LockCount | Gets the number of locks on the resource.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsResource Property Methods

The property methods of the IADsResource interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
LockCount

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_LockCount
([out] LONG *plLockCount);

 Number of locks on the resource.
Path

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Path
([out] BSTR *pbstrPath);

 The file system path of the opened resource.
User

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_User
([out] BSTR *pbstrUser);

 The name of user who opened the resource.
UserPath

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_UserPath
([out] BSTR *pbstrUserPath);

 The ADsPath of the user object for the user who opened the resource.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to examine open resources of a file service.

Dim fso as IADsFileServiceOperations

'  bind to a file service operations object on "myComputer" in the local domain.
Set fso = GetObject("WinNT://myComputer/LanmanServer")

' enumerates resources
If (IsEmpty(fso) = False) Then
    For Each resource In fso.resources
        MsgBox "Resource name: " & resource.name
        MsgBox "Resource path: " & resource.path
    Next resource
End If

Example Code [C++]

The following C++ code snippet enumerates a collection of resources.

IADsFileServiceOperations *pFso;
IADsResource *pRes;
HRESULT hr ;
LPWSTR adsPath =L"WinNT://aMachine/LanmanServer";
hr = ADsGetObject(adsPath,
                  IID_IADsFileServiceOperations,
                  (void**)&pFso);
if (FAILED(hr)) exit(hr);

IADsCollection *pColl;
hr = pFso->Resources(&pColl);
pFso->Release();

// now to enumerate the print jobs. Code omitted.

IUnknown *pUnk = NULL;
hr = pColl->get__NewEnum(&pUnk);
if (FAILED(hr) ) exit(hr);
pColl->Release();

IEnumVARIANT *pEnum;
hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
if (FAILED(hr)) exit(hr);
pUnk->Release();

// Now Enumerate
BSTR bstr;
VARIANT var;
ULONG lFetch;
IDispatch *pDisp;

VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADsResource, (void**)&pRes);

        pRes->get_Name(&bstr);
        printf("Resource name: %S\n",bstr);
        SysFreeString(bstr);

        pRes->get_Path(&bstr);
        printf("Resource path: %S\n",bstr);
        SysFreeString(bstr);

        pRes->Release();
    }
    VariantClear(&var);
    pDisp=NULL;
    hr = pEnum->Next(1, &var, &lFetch);
};

hr = pEnum->Release();

See Also

IADsFileServiceOperations::Resources


IADsServiceOperations

The IADsServiceOperations interface is designed for managing system services installed on a computer. You can use this interface to start, pause, and stop a system service, change the password, and examine the status of a given service across a network.

Of the system services and their operations, file service and file service operations are a special case. They are represented and managed by IADsFileService and IADsFileServiceOperations.

IADsServiceOperations is dual interface that inherits from IADs. The IADsServiceOperations interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsServiceOperations property methods | Description
get_Status | Gets the status of the service.
Start | Start the service.
Stop | Stop the service.
Pause | Pause the service.
Continue | Continue the service.
SetPassword | Sets password to be used by service manager to create security context.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsServiceOperations Property Methods

The property methods of the IADsServiceOperations interface read and write the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Status

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_Status
([out] LONG *pstatus)

 Status of service. The values can be one of the following:
Status
ADS_SERVICE_STOPPED
ADS_SERVICE_START_PENDING
ADS_SERVICE_STOP_PENDING
ADS_SERVICE_RUNNING
ADS_SERVICE_CONTINUE_PENDING
ADS_SERVICE_PAUSE_PENDING
ADS_SERVICE_PAUSED
ADS_SERVICE_ERROR
ADS_SERVICE_OWN_PROCESS
ADS_SERVICE_SHARE_PROCESS
ADS_SERVICE_KERNEL_DRIVER
ADS_SERVICE_FILE_SYSTEM_DRIVER
ADS_SERVICE_BOOT_START
ADS_SERVICE_SYSTEM_START
ADS_SERVICE_AUTO_START
ADS_SERVICE_DEMAND_START
ADS_SERVICE_DISABLED
ADS_SERVICE_ERROR_IGNORE
ADS_SERVICE_ERROR_NORMAL
ADS_SERVICE_ERROR_SEVERE
ADS_SERVICE_ERROR_CRITICAL 		Constant
0x00000001
0x00000002
0x00000003
0x00000004
0x00000005
0x00000006
0x00000007
0x00000008
0x00000010
0x00000020
0x00000001
0x00000002
SERVICE_BOOT_START
SERVICE_SYSTEM_START
SERVICE_AUTO_START
SERVICE_DEMAND_START
SERVICE_DISABLED
0
1
2
3

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to examine the status of Microsoft® Fax Service running on a Windows® 2000 Professional.

Dim cp As IADsComputer
Dim sr As IADsService
Dim so As IADsServiceOperations
Set cp = GetObject("WinNT://myMachine,computer")
Set sr = cp.GetObject("Service", "Fax")
Set so = sr
Select Case so.Status
    Case ADS_SERVICE_STOPPED
        MsgBox "Microsoft Fax Service is stopped."
    Case ADS_SERVICE_RUNNING
        MsgBox "Microsoft Fax Service is running."
    Case ADS_SERVICE_PAUSED
        MsgBox "Microsoft Fax Service is paused."
    Case ADS_SERVICE_ERROR
        MsgBox "Microsoft Fax Service has errors."
End Select

Example Code [C++]

The following C++ code snippet examines the status of the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

long status;
hr = pSrvOp->get_Status(&status);
switch (status) {
    case ADS_SERVICE_STOPPED:
        printf("the service is stopped.\n");
        break;
    case ADS_SERVICE_RUNNING:
        printf("the service is running.\n");
        break;
    case ADS_SERVICE_PAUSED:
        printf("the service is paused.\n");
        break;
    case ADS_SERVICE_ERROR:
        printf("the service has errors.\n");
        break;
}

pSrvOp->Release();

See Also

IADsFileService, IADsFileServiceOperations, IADsService


IADsServiceOperations::Continue

The IADsServiceOperations::Continue method resumes a service operation that has been halted by the IADsServiceOperations::Pause method.

HRESULT IADsServiceOperations::Continue(void);

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to restart a paused Microsoft Fax Service on a Windows 2000 Professional.

Dim cp As IADsComputer
Dim so As IADsServiceOperations
Set cp = GetObject("WinNT://myMachine,computer")
Set so = cp.GetObject("Service", "Fax")
If (so.Status = ADS_SERVICE_PAUSED) then ' The operation has paused.
    so.Continue
End If

Example Code [C++]

The following C++ code snippet restarts the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

long status;
hr = pSrvOp->get_Status(&status);
if(status == ADS_SERVICE_PAUSED) pSrvop->Continue();

pSrvOp->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsService


IADsServiceOperations::Pause

The IADsServiceOperations::Pause method pauses a service that has been started with the IADsServiceOperations::Start method.

HRESULT IADsServiceOperations::Pause(void);

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to pause a running Microsoft Fax Service on a Windows 2000 Professional.

Dim cp As IADsComputer
Dim so As IADsServiceOperations
Set cp = GetObject("WinNT://myMachine,computer")
Set so = cp.GetObject("Service", "Fax")
If (so.Status = ADS_SERVICE_RUNNING) then ' The operation is running.
    so.Pause
End If

Example Code [C++]

The following C++ code snippet restarts the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

long status;
hr = pSrvOp->get_Status(&status);
if(status == ADS_SERVICE_RUNNING) pSrvop->Pause();

pSrvOp->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsService


IADsServiceOperations::SetPassword

The IADsServiceOperations::SetPassword method sets the password for the account used by the service manager. This method is called when the security context for this service is created.

HRESULT IADsServiceOperations::SetPassword(
  BSTR bstrNewPassword
);

Parameters

bstrNewPassword
[in] The password to be stored as the new password.

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Remarks

The property IADsService::get_ServiceAccountName identifies the account for which this password is to be set.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to set a password for the Microsoft Fax Service on a Windows 2000 Professional.

Dim cp As IADsComputer
Dim so As IADsServiceOperations
Dim s as IADsService
Set cp = GetObject("WinNT://myMachine,computer")
Set so = cp.GetObject("Service", "Fax")
so.SetPassword "myPasswd"

set s = so
MsgBox "The password for " & so.name & " has been changed on " & s.ServiceAccountName

Example Code [C++]

The following C++ code snippet sets the password for the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

hr = pSrvOp->SetPassword(L"myPassword");

pSrvOp->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsService


IADsServiceOperations::Start

The IADsServiceOperations::Start method starts a service that is currently available on the network.

HRESULT IADsServiceOperations::Start(void);

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to start a Microsoft® Fax Service on a Windows 2000 Professional.

Dim cp As IADsComputer
Dim so As IADsServiceOperations
Set cp = GetObject("WinNT://myMachine,computer")
Set so = cp.GetObject("Service", "Fax")
If (so.Status <> ADS_SERVICE_RUNNING) then ' The operation is not running.
    so.Start
End If

Example Code [C++]

The following C++ code snippet starts the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

long status;
hr = pSrvOp->get_Status(&status);
if(status != ADS_SERVICE_RUNNING) pSrvop->Start();

pSrvOp->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsService


IADsServiceOperations::Stop

The IADsServiceOperations::Stop method stops a currently active service on the network.

HRESULT IADsServiceOperations::Stop(void);

Return Values

This method supports the standard return values, including S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to stop a running Microsoft Fax Service on a Windows 2000 Professional.

Dim cp As IADsComputer
Dim so As IADsServiceOperations
Set cp = GetObject("WinNT://myMachine,computer")
Set so = cp.GetObject("Service", "Fax")
If (so.Status = ADS_SERVICE_RUNNING) then ' The operation is running.
    so.Stop
End If

Example Code [C++]

The following C++ code snippet stops the Microsoft Fax Service running on a Windows 2000 Professional.

IADsContainer *pCont;
IADsServiceOperations *pSrvOp;
LPWSTR adsPath = L"WinNT://myMachine,computer";

HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if(FAILED(hr)) exit(hr);

IDispatch *pDisp;
hr = pCont->GetObject(L"Service",L"Fax",&pDisp);
pCont->Release();
if(FAILED(hr)) exit(hr);

hr = pDisp->QueryInterface(IID_IADsServiceOperations,(void**)&pSrvOp);
pDisp->Release();
if(FAILED(hr)) exit(hr);

long status;
hr = pSrvOp->get_Status(&status);
if(status == ADS_SERVICE_RUNNING) pSrvop->Stop();

pSrvOp->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsService


IADsSession

The IADsSession interface is designed to represent an active session for file service across a network. When a remote user opens resources on a targeted machine, an active session is established between the remote user and the targeted. Many resources can be opened in a single active session. ADSI represents this process by a session object implementing this interface.

Call the methods of this interface to examine session-specific information, for example, who is using the session, on which computer it is being used, and the connection time the user logged on for the current session.

Sessions are managed by the file service. Thus, to obtain session objects, you must first bind to this service ("LanmanServer"), as shown in the following:

Dim fso as IADsFileServiceOperations
Dim ss as IADsCollection

Set fso = GetObject("WinNT://myComputer/LanmanServer")
Set ss = fso.Sessions

IADsSession is dual interface that inherits from IADs. The IADsSession interface exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsSession property methods | Description
get_User | Gets the name of user for the session.
get_UserPath | Gets the ADsPath of the user object.
get_Computer | Gets the name of the client workstation.
get_ComputerPath | Get the ADsPath of computer object for client workstation.
get_ConnectTime | Gets the number of minutes since the session start.
get_IdleTime | Gets the number of minutes the session has been idle.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsSession Property Methods

The property methods of the IADsSession interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Computer

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_Computer
([out] BSTR *pbstrComputer);

 Name of the client workstation.
ComputerPath

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_ComputerPath
([out] BSTR *pbstrComputerPath);

 ADsPath of the computer object for the client workstation.
ConnectTime

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_ConnectTime
([out] LONG *plConnectTime);

 Number of minutes that have elapsed since the session started.
IdleTime

[Visual Basic]
Access: Read
Data Type: LONG

[C++]
HRESULT get_IdleTime
([out] LONG *plIdleTime);

 The idle time of the session, in minutes.
User

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_User
([out] BSTR *pbstrUser);

 The name of the user of the session.
UserPath

[Visual Basic]
Access: Read
Data Type: BSTR

[C++]
HRESULT get_UserPath
([out] BSTR *pbstrUserPath);

 The ADsPath of the user object for the user of this session.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to examine sessions of a file service.

Dim fso as IADsFileServiceOperations

'  bind to a file service operations object on "myComputer" in the local domain.
Set fso = GetObject("WinNT://myComputer/LanmanServer")

' enumerates sessions
If (IsEmpty(fso) = False) Then
    For Each session In fso.sessions
        MsgBox "Session Computer: " & session.Computer
        MsgBox "Session User: " & session.User
    Next Session
End If

Example Code [C++]

The following C++ code snippet enumerates a collection of sessions.

IADsFileServiceOperations *pFso;
IADsSession *pSes;
HRESULT hr ;
LPWSTR adsPath =L"WinNT://aMachine/LanmanServer";
hr = ADsGetObject(adsPath,
                  IID_IADsFileServiceOperations,
                  (void**)&pFso);
if (FAILED(hr)) exit(hr);

IADsCollection *pColl;
hr = pFso->Sessions(&pColl);
pFso->Release();

// now to enumerate sessions.

IUnknown *pUnk = NULL;
hr = pColl->get__NewEnum(&pUnk);
if (FAILED(hr) ) exit(hr);
pColl->Release();

IEnumVARIANT *pEnum;
hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&pEnum);
if (FAILED(hr)) exit(hr);
pUnk->Release();

// Now Enumerate
BSTR bstr;
VARIANT var;
ULONG lFetch;
IDispatch *pDisp;

VariantInit(&var);
hr = pEnum->Next(1, &var, &lFetch);
while(hr == S_OK)
{
    if (lFetch == 1)
    {
        pDisp = V_DISPATCH(&var);
        pDisp->QueryInterface(IID_IADsSession, (void**)&pSes);

        pSes->get_Computer(&bstr);
        printf("Session host: %S\n",bstr);
        SysFreeString(bstr);

        pSes->get_User(&bstr);
        printf("Session user: %S\n",bstr);
        SysFreeString(bstr);

        pRes->Release();
    }
    VariantClear(&var);
    pDisp=NULL;
    hr = pEnum->Next(1, &var, &lFetch);
};

hr = pEnum->Release();

See Also

IADsFileServiceOperations::Sessions


Security Interfaces

This section describes the following security interfaces:

  • IADsAccessControlEntry
  • IADsAccessControlList
  • IADsSecurityDescriptor


IADsAccessControlEntry

The IADsAccessControlEntry interface enables directory clients to access and manipulate individual access-control entries (ACEs) of the owning object. An ACE stipulates who can access the object and what type of access will be granted. It also specifies whether the access control settings can be propagated from the object to any of its children. An ACE exposes a set of properties through this interface to provide such services.

An object can have a number of ACEs, one for each client or a group of clients. ACEs are maintained in an access-control list (ACL) which implements the IADsAccessControlList interface. This means that a client needs to go through an ACL in order to access ACEs. To access the ACL, one must obtain the object's security descriptor that implements the IADsSecurityDescriptor interface. The general procedure to manage access controls over an ADSI object involves the following steps:

  1. Obtain the security descriptor of the object of interest.
  2. Retrieve an ACL from the security descriptor.
  3. Work with ACEs in the ACL.

To make any new or modified ACEs persistent:

  1. Add the ACEs to the ACL
  2. Assign the ACL to the security descriptor
  3. Commit the security descriptor to the directory store.

The IADsAccessControlEntry interface is a dual interface and exposes the following property methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets/sets one of its properties.

IADsAccessControlEntry property methods | Description
get/put_AccessMask | Gets/sets the access mask for an ACE.
get/put_AceType | Gets/sets the ACE type.
get/put_AceFlags | Gets/sets ACE flag values.
get/put_Flags | Gets/sets flag values.
get/put_ObjectType | Gets/sets the object type.
get/put_InheritedObjectType | Gets/sets the inherited object type.
get/put_Trustee | Gets/sets the trustee.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IAccessControlList, IADsSecurityDescriptor, Using Credentials and Security


IADsAccessControlEntry Property Methods

The property methods of the IADsAccessControlEntry interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
AccessMask

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AccessMask
([out] LONG *plnAccessMask);

HRESULT put_AccessMask
([in] LONG lnAccessMask);

 A flag specifying access permissions. Valid values are defined in ADS_RIGHTS_ENUM.
AceType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AceType
([out] LONG *plAceType);

HRESULT put_AceType
([in] LONG lnAceType);

 A flag indicating ACE types. Valid values are defined in ADS_ACETYPE_ENUM.
AceFlags

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AceFlags
([out] LONG *plnAceFlags);

HRESULT put_AceFlags
([in] LONG lnAceFlags);

 A flag specifying whether other containers or objects can inherit the ACE from the owner of the ACL. Valid values are defined in ADS_ACEFLAG_ENUM.
Flags

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Flags
([out] LONG *lnflags);

HRESULT put_Flags
([in] LONG lnflags);

 A flag indicating whether the ACE has an object type or inherited object type. Valid flags are defined in ADS_FLAGTYPE_ENUM.
ObjectType

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ObjectType
([out] BSTR *bstrObjectType);

HRESULT put_ObjectType
([in] BSTR bstrObjectType);

 A flag indicating the type of an ADSI object. Its value is a GUID to a property or an object in string format. The GUID refers to a property when ADS_RIGHT_DS_READ_PROP and ADS_RIGHT_DS_WRITE_PROP access masks are used. The GUID specifies an object when ADS_RIGHT_DS_CREATE_CHILD and ADS_RIGHT_DS_DELETE_CHILD access masks are used.
InheritedObjectType

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_InheritedObjectType
([out] BSTR *bstrInheritedObjectType);

HRESULT put_InheritedObjectType
([in] BSTR bstrInheritedObjectType);

 A flag indicating the type of a child object of an ADSI object. Its value is a GUID to an object in string format. When such a GUID is set, the ACE applies only to the object referred to by the GUID.
Trustee

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Trustee
([out] BSTR *pbstrSecurityId);

HRESULT put_Trustee
([in] BSTR bstrSecurityId);

 A user path who is granted access permissions as set in the ACE of the object.

Example Code [Visual Basic]

The following Visual Basic® code snippet illustrates how to add entries to a discretionary ACL using the IADsAccessControlEntry property methods:

----- VB Example Adding ACEs to DACL-----
Dim x As IADs
Dim sd As IADsSecurityDescriptor
Dim ace As IADsAccessControlEntry
Dim Dacl As IADsAccessControlList
Dim Ace1 As New AccessControlEntry
Dim Ace2 As New AccessControlEntry

Set x = GetObject("LDAP://OU=Sales, DC=ArcadiaBay,DC=com")
Set sd = x.Get("ntSecurityDescriptor")
Set Dacl = sd.DiscretionaryAcl

'Show existing ACEs
For Each ace In Dacl
  Debug.Print ace.Trustee
Next

' Set up the first ACE
Ace1.AccessMask = -1 'Full Permission (Allowed)
Ace1.AceType = ADS_ACETYPE_ACCESS_ALLOWED
Ace1.AceFlags = ADS_ACEFLAG_INHERIT_ACE
Ace1.Trustee = "ACTIVED\Administrator"

' Set up the 2nd ACE
Ace2.AccessMask = -1 'Full Permission (Denied)
Ace2.AceType = ADS_ACETYPE_ACCESS_DENIED
Ace2.AceFlags = ADS_ACEFLAG_INHERIT_ACE
Ace2.Trustee = "ACTIVED\Andyhar"

' Add the ACEs to the Discretionary ACL
Dacl.AddAce Ace1
Dacl.AddAce Ace2

sd.DiscretionaryAcl = Dacl
x.Put "ntSecurityDescriptor", Array(sd)
x.SetInfo

Example Code [C++]

The following C++ code snippet displays access-control entries:

LONG aceMask;
LONG aceType;
int idx;
BSTR bstr;
CString sObjectType;

ASSERT( pACE );

///////////////////////////////////////////////////
//Get Access Mask, Ace Type, and Object Type
///////////////////////////////////////////////////
if( !SUCCEEDED(pACE->get_AccessMask(&aceMask)) )
{
    return;
}
if ( !SUCCEEDED(pACE->get_AceType(&aceType)) )
{
   return;
}

if ( !SUCCEEDED(pACE->get_ObjectType(&bstr)) )
{
   return;
}

sObjectType = bstr;
SysFreeString( bstr );

//////////////////////////////////////
// Display the type
//////////////////////////////////////
printf("AceType: %d", aceType );

// Standard ACE Rights
if ( aceMask & ADS_RIGHT_DELETE )
{
printf("Right to Delete");
}

if ( aceMask & ADS_RIGHT_READ_CONTROL )
{
printf("Right to Read Control");
}

if ( aceMask & ADS_RIGHT_WRITE_DAC )
{
   printf("Right to Write Control");
}

if ( aceMask & ADS_RIGHT_WRITE_OWNER )
{
   printf("Right to Take Ownership");

}

// Directory ACE Rights
if ( aceMask & ADS_RIGHT_DS_CREATE_CHILD )
{
   DisplayAceObjectType( aceMask, sObjectType );
}

if ( aceMask & ADS_RIGHT_DS_DELETE_CHILD )
{
   DisplayAceObjectType( aceMask, sObjectType );
}

if ( aceMask & ADS_RIGHT_ACTRL_DS_LIST )
{
   printf("List Content");
}

if ( aceMask & ADS_RIGHT_DS_SELF )
{
printf("List Object");
}

if ( aceMask & ADS_RIGHT_DS_DELETE_TREE )
{
printf("Delete Tree");
}

if ( aceMask & ADS_RIGHT_DS_READ_PROP )
{
printf("Read Property :");
DisplayAceObjectType( aceMask, sObjectType );
}

if ( aceMask & ADS_RIGHT_DS_WRITE_PROP )
{
   printf("Write Property: ");
   DisplayAceObjectType( aceMask, sObjectType );
}

void DisplayAceObjectType( LONG aceMask, CString &sObjectType )
{
if ( sObjectType.IsEmpty() )
{
   printf("ALL");
}
else
{
// This should be a GUID (object's, attribute's or extended right's GUID)
   printf( "%s", sObjectType );
}
}

See Also

IADsAccessControlList, IADsSecurityDescriptor


IADsAccessControlList

The IADsAccessControlList interface manages individual access-control entries (ACEs). An access-control list (ACL) consists of a collection of ACEs that can provide more fine-tuned access control to the same ADSI object for different clients. In general, different providers implement different access controls. The object's behavior is thus highly provider-specific. Consult the documentation of your provider for more information. For providers supplied by Microsoft, see ADSI System Providers. Presently, only the LDAP provider supports access controls.

Before you can work with an object's ACEs, you must first obtain the ACL to which they belong. ACLs are managed by security descriptors and can be of either discretionary ACL and system ACL. For more information, see the topic of Using Credentials and Security and the IADsSecurityDescriptor interface.

Using the properties and methods of the IADsAccessControlList interface, you can retrieve and enumerate ACEs, add new entries to the list, or remove existing ones. The general procedure to manage access controls over an ADSI object involves the following steps:

  1. Obtain the security descriptor of the object of interest.
  2. Retrieve an ACL from the security descriptor.
  3. Work with ACEs in the ACL.

To make any new or modified ACEs persistent:

  1. Add the ACEs to the ACL
  2. Assign the ACL to the security descriptor
  3. Commit the security descriptor to the directory store.

The code snippets presented in the following table and in the IADsAccessControlEntry interfaces provide working examples.

The IADsAccessControlList is a dual interface and exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets/sets one of its properties.

IADsAccessControlList property methods | Description
get/put_AclRevision | Gets/sets the ACL revision number.
get/put_AceCount | Gets/sets number of ACEs in the ACL.
AddAce | Adds an entry to the ACL.
RemoveAce | Removes an entry from the ACL.
CopyAccessList | Copies the ACL.
get__NewEnum | Gets an pointer to the enumerator object.

Example Code [Visual Basic]

The following Visual Basic code snippet shows the general procedure to work with access control entries of a discretionary ACL.

Dim X As IADs
Dim Namespace As IADsOpenDSObject
Dim SecurityDescriptor As IADsSecurityDescriptor
Dim Dacl As IADsAccessControlList

Set Namespace = GetObject("LDAP://")
Set X= Namespace.OpenDSObject("LDAP://DC=ArcadiaBay, DC=Com, "cn=administrator,cn=users,DC=ArcadiayBay, DC=COM ", "passwOrdGoesHere",  ADS_SECURE_AUTHENTICATION)

Set SecurityDescriptor = X.Get("ntSecurityDescriptor")
Debug.Print SecurityDescriptor.Owner
Debug.Print SecurityDescriptor.Group

Set Dacl = SecurityDescriptor.DiscretionaryAcl
Debug.Print Dacl.AceCount

For Each Obj In Dacl
   Debug.Print Obj.Trustee
   Debug.Print Obj.AccessMask
   Debug.Print Obj.AceFlags
   Debug.Print Obj.AceType
Next

Example Code [C++]

The following C++ code snippet enumerates ACEs from a DACL.

HRESULT hr;
VARIANT var;

// Assume that the pADs (IADs) pointer is valid

hr = pADs->Get(L"ntSecurityDescriptor", &var );

if (!SUCCEEDED(hr) )
{
   return;
}

///////////////////////////////////////
// Get the IADsSecurityDescriptor
///////////////////////////////////////
IDispatch *pDisp;
IADsSecurityDescriptor *pSecurityDesc;
pDisp = V_DISPATCH(&var);
hr = pDisp->QueryInterface( IID_IADsSecurityDescriptor, (void**) &pSecurityDesc );
VariantClear(&var);
if ( !SUCCEEDED(hr) )
{
   return;
}

////////////////////////////////////////
// Get the ACL
////////////////////////////////////////
IADsAccessControlList *pACL;
hr = pSecurityDesc->get_DiscretionaryAcl( &pDisp );
if (!SUCCEEDED(hr) )
{
   pSecurityDesc->Release();
   return;
}

hr=pDisp->QueryInterface(IID_IADsAccessControlList, (void**) &pACL);
pDisp->Release();
pSecurityDesc->Release();

//////////////////////////////////////////////
// Enumerate ACEs
//////////////////////////////////////////////
IEnumVARIANT *pEnum;
LPUNKNOWN     pUnk;
ULONG    lFetch;
BSTR    bstr;
IADsAccessControlEntry *pACE;

hr = pACL->get__NewEnum( &pUnk );
if ( !SUCCEEDED(hr) )
{
   pACL->Release();
   return;
}

hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
if ( !SUCCEEDED(hr) )
{
   return;
}

hr = pEnum->Next( 1, &var, &lFetch );

while( hr == S_OK )
{
   if ( lFetch == 1 )
   {
      if ( VT_DISPATCH != V_VT(&var) )
      {
         pEnum->Release();
         return;
      }
      pDisp = V_DISPATCH(&var);
      /////////////////////////////
      // Get the individual ACE
      /////////////////////////////
      hr = pDisp->QueryInterface( IID_IADsAccessControlEntry,
                                  (void**)&pACE );
      if ( SUCCEEDED(hr) )
      {
         pACE->get_Trustee(&bstr);
         printf("\n %S:\n", bstr);
         // ACE manipulation here
         SysFreeString(bstr);
         pACE->Release();
      }
       VariantClear(&var);
   }
   hr = pEnum->Next( 1, &var, &lFetch );
};

pACL->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry, IADsSecurityDescriptor, Using Credentials and Security


IADsAccessControlList Property Methods

The property methods of the IADsAccessControlList interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
AclRevision

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AclRevision
([out] LONG *lnAclRevision);

HRESULT put_AclRevision
([in] LONG lnAclRevision);

 The revision level of an access-control list. This value should be ACL_REVISION. All ACEs in an ACL must be at the same revision level.
AceCount

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get AceCount
([out] LONG *lnAceCount);

HRESULT put AceCount
([in] LONG lnAceCount);

 The number of access control entries in the access-control list.

Example Code [Visual Basic]

The following Visual Basic code snippet displays the number of ACEs in an ACL.

Set x = GetObject("LDAP://OU=Sales, DC=ArcadiaBay,DC=com")
Set sd = x.Get("ntSecurityDescriptor")
Set Dacl = sd.DiscretionaryAcl
Debug.Print Dacl.AceCount

Example Code [C++]

The following C++ code snippet displays the number of ACEs in an ACL.

IADs *pObj;
LPWSTR guestPath = L""LDAP://OU=Sales, DC=ArcadiaBay,DC=com";
LPWSTR user = L"Power User";
LPWSTR passwd = L"Top Secret";

HRESULT hr;

hr = ADsOpenObject(guestPath,user,passwd,1,IID_IADs,(void**)&pObj);
if(FAILED(hr)) {
    printf("hr = %x\n",hr);
    return hr;
}
else {
    BSTR bstr;
    pObj->get_Class(&bstr);
    printf("Object class: %S\n",bstr);
    SysFreeString(bstr);
}

VARIANT var;
VariantInit(&var);
hr = pObj->Get(L"ntSecurityDescriptor",&var);
pObj->Release();

if(FAILED(hr)) {
    printf("Get ntSD: hr = %x\n",hr);
    return hr;
}

IADsSecurityDescriptor *psd;
hr = V_DISPATCH(&var)->QueryInterface(IID_IADsSecurityDescriptor,
                                      (void**)&psd);

if(FAILED(hr)) {
    printf("DISP: hr = %x\n",hr);
    return hr;
}

IDispatch *pDisp;
hr = psd->get_DiscretionaryAcl(&pDisp);
VariantClear(&var);

if(FAILED(hr)) {
    printf("get_DACL : hr = %x\n",hr);
    return hr;
}

IADsAccessControlList *pAcl;
hr = pDisp->QueryInterface(IID_IADsAccessControlList,(void**)&pAcl);
pDisp->Release();

if(FAILED(hr)) {
    printf("QI ACL: hr = %x\n",hr);
    return hr;
}

long count;
hr = pAcl->get_AceCount(&count);
if(FAILED(hr)) {
    printf("Count: hr = %x\n",hr);
    return hr;
}

printf("AceCount = %d\n",count);

pAcl->Release();
return hr;

See Also

IEnumVARIANT, IADsAccessControlEntry, IADsSecurityDescriptor


IADsAccessControlList::AddAce

The IADsAccessControlList::AddAce method adds one or more access control entries (ACEs) to the access-control list (ACL).

HRESULT AddAce(
  IDispatch * pAccessControlEntry
);

Parameters

[in] pAccessControlEntry
Array of ACEs to be added to the ACL. Cannot be NULL.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The ACE was added successfully.
E_OUTOFMEMORY
No memory can be allocated for the new ACEs.
E_FAIL
The operation has failed.
E_INVALIDARG
The supplied argument is not valid.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet illustrates how to use the IADsAccessControlList::AddAce method to add two ACEs to an ACL.

Dim Ace1 as new IADsAccessControlEntry
Dim Ace2 As new IADsAccessControlEntry
Dim Dacl as new IADsAccessControlList
' Add the ACEs to the Disretionary ACL

Dacl.AclRevision = ADS_SD_REVISION_DS 'DS ACL Revision
' Set up the first ACE
Ace1.AccessMask = -1 'Full Permission (Allowed)
Ace1.AceType = ADS_ACETYPE_ACCESS_ALLOWED
Ace1.AceFlags = ADS_ACEFLAG_INHERIT_ACE
Ace1.Trustee = "myMachine\Administrator"

' Set up the 2nd ACE
Ace2.AccessMask = -1 'Full Permission (Denied)
Ace2.AceType = ADS_ACETYPE_ACCESS_DENIED
Ace2.AceFlags = ADS_ACEFLAG_INHERIT_ACE
Ace2.Trustee = "aDomain\aUser"

' Add the ACEs to the Disretionary ACL
Dacl.AddAce Ace1
Dacl.AddAce Ace2

'Commit the changes
sd.DiscretionaryAcl = Dacl
x.Put "ntSecurityDescriptor", Array(sd)
x.SetInfo

Example Code [C++]

The following C++ code snippet adds an ACE to an ACL using the IADsAccessControlList::AddAce method. The added ACE has allowed access rights with the full permission.

HRESULT addAceTo(IADsAccessControlList *pAcl)
{
    if(!pAcl) return E_FAIL;
    HRESULT hr = pAcl->put_AclRevision(ADS_SD_REVISION_DS);
    if(FAILED(hr)) return hr;

    IADsAccessControlEntry *pAce;
    pAce = createAce(-1,                    // full permissions
                     ADS_ACETYPE_ACCESS_ALLOWED,
                     ADS_ACEFLAG_INHERIT_ACE,
                     L"aDomain\aUser");

    if(!pAce) return E_FAIL;

    IDispatch *pDisp;
    hr = pAce->QueryInterface(IID_IDispatch,(void**)&pDisp);
    if(FAILED(hr)) {
        pAce->Release();
        return hr;
    }

    hr = pAcl->AddAce(pDisp);
    pDisp->Release();
    if(FAILED(hr)) return hr;

    printf("Ace has been added to ACL.\n");
    if(pAce) pAce->Release();

    return S_OK;
}
////////////////////////////////////
// function to create an allowed ACE
////////////////////////////////////
IADsAccessControlEntry *createAce(
                   long mask,
                   long type,
                   long flag,
                   BSTR trustee)
{
    HRESULT hr;
    IADsAccessControlEntry *pAce;
    hr = CoCreateInstance(CLSID_AccessControlEntry,
                          NULL,
                          CLSCTX_INPROC_SERVER,
                          IID_IADsAccessControlEntry,
                          (void**)&pAce);
    if(FAILED(hr)) {
        if(pAce) pAce->Release();
        return NULL;
    }

    hr = pAce->put_AccessMask(mask);
    hr = pAce->put_AceType(type);
    hr = pAce->put_AceFlags(flag);
    hr = pAce->put_Trustee(trustee);
    return pAce;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry


IADsAccessControlList::CopyAccessList

The IADsAccessControlList::CopyAccessList method copies all access control entries (ACEs) in the access-control list (ACL) to the caller's process space.

HRESULT CopyAccessList(
  IDispatch ** ppAccessControlList
);

Parameters

ppAccessControlList
[out] Address of an IDispatch interface pointer to an ACL as the copy of the original access list. If this parameter is NULL on return, no copies of the ACL could be made.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The ACEs was copied successfully.
E_INVALIDARG
The argument supplied is not valid.
OUTOFMEMORY
No memory could be allocated for the new ACL.
E_FAIL
The operation has failed.

For other return values, see ADSI Error Codes.

Remarks

As with any method-allocated memory, the caller is responsible for calling Release on the copy of ACEs through their IDispatch pointers.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to copy ACL from one ADSI object to another.

Dim x As IADs
Dim sd As IADsSecurityDescriptor
Dim Dacl As IADsAccessControlList
Dim CopyDacl As IADsAccessControlList

'Get the ACL from one object
Set x = GetObject("LDAP://OU=Sales, DC=activeD,DC=nttest,DC=microsoft,DC=com")
Set sd = x.Get("ntSecurityDescriptor")
Set Dacl = sd.DiscretionaryAcl
Set CopyDacl = Dacl.CopyAccessList()

'Copy the ACL to another object in the Directory
Set x = GetObject("LDAP://OU=Sales, DC=AracadiaBay,DC=com")
Set sd = x.Get("ntSecurityDescriptor")
sd.DiscretionaryAcl = CopyDacl
x.Put "ntSecurityDescriptor", Array(sd)
x.SetInfo

Example Code [C++]

The following C++ code snippet makes a copy of an ACL.

LPWSTR guestPath = L"LDAP://CN=Guest,CN=Sales,dc=ArcardiaBay,dc=com";
LPWSTR user = L"Administrator";
LPWSTR passwd = L"secret";

IADs *pAds;
HRESULT hr;
hr = ADsOpenObject(guestPath,
                   user,
                   passwd,
                   ADS_SECURE_AUTHENTICATION,
                   IID_IADs,
                   (void**)&pAds);
if(FAILED(hr)) exit(0);

IADsSecurityDescriptor *pSD=getSD(pAds);
if(!pSD) {
    pAds->Release();
    exit (0);
}

IADsAccessControlList *pAcl = getAcl(pSD);

IADsAccessControlList *pAclCopy;
hr = pAcl.CopyAccessList(pAclCopy);

IADs pValCust;
hr = ADsOpenObject(
          L"LDAP://CN=ValuedCustomer,CN=Sales,DC=ArcardiaBay,DC=com",
          user,
          passwd,
          ADS_SECURE_AUTHENTICATION,
          IID_IADs,
          (void**) &pValCust);

IADsSecurityDescriptor *pSdValCust=getSD(pValCust);

IDispatch *pDisp;
hr = pAclCopy->QueryInterface(IID_IDispatch,(void**)&pDisp);
hr = pSdValCust->put_DiscretionaryACL(pDisp);
hr = pValCust->SetInfo();

pDisp->Release();

pValCust->Release();
pAclCopy->Release();
pSdValCust->Release();

if(pAcl) pAcl->Release();
if(pSD)   pSD->Release();
if(pAds) pAds->Release();

/////////////////////////////////////////////////////////
// functions to bind to an object and get its SD and ACL.
/////////////////////////////////////////////////////////
IADs *getIADsObject(LPWSTR adsPath,LPWSTR usr,LPWSTR passwd)
{
    if(!adsPath) return NULL;
    HRESULT hr;
    IADs *pObj;
    hr = ADsOpenObject(adsPath,
                       usr,
                       passwd,
                       ADS_SECURE_AUTHENTICATION,
                       IID_IADs,
                       (void**)&pObj);

    if(FAILED(hr)) {
        printf("adsopenobject: hr = %x\n",hr);
        if(pObj) pObj->Release();
        return NULL;
    }
    return pObj;
}

IADsSecurityDescriptor *getSD(IADs *pObj)
{
    VARIANT var;
    VariantInit(&var);
    HRESULT hr;

    hr = pObj->Get(L"ntSecurityDescriptor",&var);
    if(FAILED(hr)) {
        VariantClear(&var);
        return NULL;
    }

    IADsSecurityDescriptor *psd;
    hr = V_DISPATCH(&var)->QueryInterface(IID_IADsSecurityDescriptor,
                                  (void**)&psd);
    if(FAILED(hr)) {
        if(psd) psd->Release();
        return NULL;
    }
    return psd;
}

IADsAccessControlList *getAcl(IADsSecurityDescriptor *psd)
{
    HRESULT hr;
    IDispatch *pDisp;

    hr = psd->get_DiscretionaryAcl(&pDisp);
    if(FAILED(hr)) {
    pDisp->Release();
    return NULL;
    }

    IADsAccessControlList *pAcl;
    hr = pDisp->QueryInterface(IID_IADsAccessControlList,
                               (void**)&pAcl);
    pDisp->Release();
    if(FAILED(hr)) {
       pAcl->Release();
       return NULL;
    }

    return pAcl;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry, IADsSecurityDescriptor


IADsAccessControlList::RemoveAce

The IADsAccessControlList::RemoveAce method removes an access-control entry(ACE) from the access-control list (ACL).

HRESULT RemoveAce(
  IDispatch * pAccessControlEntry
);

Parameters

pAccessControlEntry
[in] ACEs to be removed from the ACL.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The ACEs were successfully removed.
E_FAIL
The operation has failed.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to remove entries from a discretionary access-control list.

Dim x As IADs
Dim sd As IADsSecurityDescriptor
Dim Dacl As IADsAccessControlList

Set x = GetObject("LDAP://OU=Sales, DC=activeD,DC=nttest,DC=microsoft,DC=com")
Set sd = x.Get("ntSecurityDescriptor")
Set Dacl = sd.DiscretionaryAcl

'—Removing ACEs belong to jsmith ----
For Each ace In Dacl
  If (ace.Trustee = "ARCADIABAY\jsmith") Then
      Dacl.RemoveAce (ace)
  End If
Next

sd.DiscretionaryAcl = Dacl
x.Put "ntSecurityDescriptor", Array(sd)
x.SetInfo

Example Code [C++]

The following C++ code snippet removes the ACEs with the specified Trustee ("aDomain\PowerUser").

HRESULT removeAcesFrom(IADsAccessControlList *pAcl)
{
    IEnumVARIANT *pEnum;
    LPUNKNOWN     pUnk;
    ULONG  lFetch;
    BSTR    bstr;
    IADsAccessControlEntry *pACE;

    HRESULT hr = pAcl->get__NewEnum( &pUnk );
    if ( !SUCCEEDED(hr) )
    {
        pAcl->Release();
        return hr;
    }

    hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
    if ( !SUCCEEDED(hr) )
    {
        return hr;
    }

    VARIANT var;
    VariantInit(&var);
    hr = pEnum->Next( 1, &var, &lFetch );
    IDispatch *pDisp;

    while( hr == S_OK )
    {
       if ( lFetch == 1 )
       {
           if ( VT_DISPATCH != V_VT(&var) )
           {
               pEnum->Release();
               return hr;
           }
           pDisp = V_DISPATCH(&var);
           ///////////////////////////
           // Get the individual ACE
           ///////////////////////////
           hr = pDisp->QueryInterface( IID_IADsAccessControlEntry,
                                       (void**)&pACE );
           if ( SUCCEEDED(hr) )
           {
               pACE->get_Trustee(&bstr);
               printf("ACE trustee: %S:\n", bstr);
               //ACE manipulation here
               SysFreeString(bstr);
              if(wcscmp(bstr,L"aDomain\PowerUser")==0)
              {
                   pACE->QueryInterface(IID_IDispatch,(void**)&pDisp);
                   hr = pAcl->RemoveAce(pDisp);
                   if(FAILED(hr))
                   {
                       pDisp->Release();
                       return hr;
                   }
              }
              pACE->Release();
           }
           VariantClear(&var);
       }
       hr = pEnum->Next( 1, &var, &lFetch );
    }
    return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry IADsSecurityDescriptor


IADsAccessControlList::get__NewEnum

The IADsAccessControlList::get__NewEnum method is used to obtain an enumerator object for the ACL to enumerate ACEs.

HRESULT get__NewEnum(
  IUnknown ** ppEnumerator
);

Parameters

ppEnumerator
[out] Pointer to the IUnknown interface used to query the IEnumVARIANT interface on an enumerator object for the ACL.

Return Values

This method returns the standard return values, including S_OK and E_FAIL. For other return values, see ADSI Error Codes.

Remarks

Notice that there are two underscores in get__NewEnum.

Example Code [Visual Basic]

Visual Basic makes an implicit call to the get__NewEnum method in the execution of the For Each obj In collection … Next obj statement.

Dim Dacl As IADsAccessControlList
Dim ace as IADsAccessControlEntry

' Do something to get Dacl, code omitted.

' Remove all the ace's from the acl.
For Each ace In Dacl
    Dacl.RemoveAce (ace)
Next ace

Example Code [C++]

C/C++ code will be more involved. The following C++ code snippet illustrates how to enumerate ACEs using IADsAccessControlList::get__NewEnum.

IEnumVARIANT *pEnum;
LPUNKNOWN     pUnk;
ULONG  lFetch;
BSTR    bstr;
IADsAccessControlEntry *pACE;

hr = pACL->get__NewEnum( &pUnk );
if ( !SUCCEEDED(hr) )
{
   pACL->Release();
   return;
}

hr = pUnk->QueryInterface( IID_IEnumVARIANT, (void**) &pEnum );
if ( !SUCCEEDED(hr) )
{
   return;
}

hr = pEnum->Next( 1, &var, &lFetch );

while( hr == S_OK )
{
   if ( lFetch == 1 )
   {
       if ( VT_DISPATCH != V_VT(&var) )
       {
           pEnum->Release();
           return;
       }
       pDisp = V_DISPATCH(&var);
       /////////////////////////
       // Get the individual ACE
       /////////////////////////
       hr = pDisp->QueryInterface( IID_IADsAccessControlEntry,
                                  (void**)&pACE );
      if ( SUCCEEDED(hr) )
      {
          pACE->get_Trustee(&bstr);
          printf("\n %S:\n", bstr);
          //ACE manipulation here
          SysFreeString(bstr);
          pACE->Release();
      }
      VariantClear(&var);
   }
hr = pEnum->Next( 1, &var, &lFetch );
};

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IEnumVARIANT, IADsAccessControlEntry, IADsSecurityDescriptor


IADsSecurityDescriptor

The IADsSecurityDescriptor interface provides access to properties on an ADSI security descriptor object. You use this interface to examine and change the access controls to an directory service object. You can also use it to make copies of a security descriptor. You use an object's ntSecurityDescriptor property to access its security descriptor object.

The IADsSecurityDescriptor interface is a dual interface and exposes the following properties and methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods, or gets/sets one of its properties.

IADsSecurityDescriptor property methods | Description
get/put_Revision | Gets/puts the revision number assigned to the security descriptor.
get/put_Control | Gets/puts the Security_Descriptor_Control flag.
get/put_Owner | Gets/puts the owner of the object associated with the security descriptor.
get/put_OwnerDefaulted | Gets/puts the flag that indicates if the owner information is derived by a default mechanism.
get/put_Group | Gets/puts the group that owns the object associated with the security descriptor.
get/put_GroupDefaulted | Gets/puts the flag that indicates if the group information is derived by a default mechanism.
get/put_DiscretionaryAclt | Gets/puts the discretionary ACL associated with the security descriptor.
get/put_DaclDefaulted | Gets/puts the flag that indicates if the DACL is derived from a default mechanism.
get/put_SystemAcl | Gets/puts the system ACL (SACL) associated with the security descriptor.
get/put_SaclDefaulted | Gets/puts the flag that indicates if the SACL is derived from a default mechanism.
CopySecurityDescriptor | Copies the security descriptor.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to manipulate a security descriptor.

'--- VB Example: Getting the security descriptor
Dim x As IADs
Dim sd As IADsSecurityDescriptor
Dim acl As IADsAccessControlList

Set x = GetObject("LDAP://DC=ArcadiaBay, DC=com")
Set sd = x.Get("ntSecurityDescriptor")
Debug.Print sd.Control
Debug.Print sd.Group
Debug.Print sd.Owner
Debug.Print sd.Revision

Set acl = sd.DiscretionaryAcl
Set sacl = sd.SystemAcl

Example Code [C++]

The following C++ code snippet works with the security descriptor of a directory object.

IADs *getIADsObject(LPWSTR,LPWSTR,LPWSTR);
IADsSecurityDescriptor *getSD(IADsSecurityDescriptor *);

HRESULT workWithSD()
{
   LPWSTR adsPath = L"LDAP://CN=JSmith,CN=Users,dc=ArcadiaBay,dc=com";
   LPWSTR user = L"Administrator";
   LPWSTR passwd = L"";

   HRESULT hr;

   IADs *pAds = getIADsObject(adsPath,user,passwd);
   if(!pAds) exit (0);

   IADsSecurityDescriptor *pSD=getSD(pAds);
   if(!pSD) {
       pAds->Release();
       exit (0);
   }

   BSTR bstr;
   long lVal;
   hr = pSD->get_Control(&lVal);
   printf("SD Control = %d\n",lVal);

   hr = pSD->get_Owner(&bstr);
   printf("SD Owner   = %S\n",bstr);
   SysFreeString(bstr);

   hr = pSD->get_Group(&bstr);
   printf("SD Group   = %S\n",bstr);
   SysFreeString(bstr);

   hr = pSD->get_Revision(&lVal);
   printf("SD Revision= %d\n",lVal);

   IDispatch *pDisp;
   IADsAccessControlList *dacl, *sacl;
   hr = pSD->get_DiscretionaryAcl(&pDisp);
   hr = pDisp->QueryInterface(IID_IADsAccessControlList,
                              (void**)&dacl);
   hr = pDisp->Release();

// Work with access control list, code omitted.

   if(dacl) dacl->Release();
   if(pSD)  pSD->Release();
   if(pAds) pAds->Release();
   return S_OK;
}

/////////////////////////////////////////////////////////
// functions to bind to an object and get its SD and ACL.
/////////////////////////////////////////////////////////
IADs *getIADsObject(LPWSTR adsPath,LPWSTR usr,LPWSTR passwd)
{
    if(!adsPath) return NULL;
    HRESULT hr;
    IADs *pObj;
    if(!usr) {
         hr = ADsGetObject(adsPath,IID_IADs,(void**)&pObj);
         if(FAILED(hr)) {
             if(pObj) pObj->Release();
             return NULL;
         }
    }
    else {
        hr = ADsOpenObject(adsPath,
                          usr,
                          passwd,
                          1,
                          IID_IADs,
                          (void**)&pObj);

        if(FAILED(hr)) {
            printf("adsopenobject: hr = %x\n",hr);
            if(pObj) pObj->Release();
            return NULL;
        }
    }
    return pObj;
}
///////////////////////////////////////
// IADsSecurityDescriptor *getSD(IADs*)
///////////////////////////////////////
IADsSecurityDescriptor *getSD(IADs *pObj)
{
    VARIANT var;
    VariantInit(&var);
    HRESULT hr;

    hr = pObj->Get(L"ntSecurityDescriptor",&var);
    if(FAILED(hr)) {
        VariantClear(&var);
        return NULL;
    }

    IADsSecurityDescriptor *psd;
    hr = V_DISPATCH(&var)->QueryInterface(IID_IADsSecurityDescriptor,
                                  (void**)&psd);
    if(FAILED(hr)) {
        if(psd) psd->Release();
        return NULL;
    }
    return psd;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry, IADsAccessControlList, Using Credentials and Security


IADsSecurityDescriptor Property Methods

The property methods of the IADsSecurityDescriptor interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Revision

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Revision
([out] LONG *plRevision);

HRESULT put_Revision
([in] LONG lRevision);

 Revision level of the security descriptor. This value is taken from the Win32 ACL_REVISION_INFORMATION structure. All ACEs in an ACL must be at the same revision level.
Control

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Control
([out] LONG *plControl);

HRESULT put_Control
([in] LONG lControl);

 Flags that qualify the meaning of the security descriptor. Values are taken from the Win32 SECURITY_DESCRIPTOR_CONTROL structure.
Owner

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Owner
([out] BSTR *pbstrOwnerl);

HRESULT put_Owner
([in] BSTR bstrOwner);

 Owner of the object.
OwnerDefaulted

[Visual Basic]
Access: Read/Write
Data Type: VARIANT_BOOL

[C++]
HRESULT get_OwnerDefaulted
([out] VARIANT_BOOL *fOwnerDefaulted);

HRESULT put_OwnerDefaulted
([in] VARIANT_BOOL fOwnerDefaulted);

 A flag of the BOOL type to indicate if the owner information is derived from a default mechanism, rather than by the original provider of the security descriptor explicitly.
Group

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Group
(BSTR *pbstrGroupl);

HRESULT put_Group
(BSTR bstrGroup);

 Group to which the owner's security ID belongs.
GroupDefaulted

[Visual Basic]
Access: Read/Write
Data Type: VARIANT_BOOL

[C++]
HRESULT get_GroupDefaultedY
([out] VARIANT_BOOL *fGroupDefaulted);

HRESULT put_GroupDefaulted
([out] VARIANT_BOOL fGroupDefaulted);

 A flag of the BOOL type to indicate if the group information is derived from a default mechanism, rather than by the original provider of the security descriptor explicitly.
DiscretionaryAcl

[Visual Basic]
Access: Read/Write
Data Type: IDISPATCH

[C++]
HRESULT get_DiscretionaryAcl
([out] IDispatch **ppIDispDACL);

HRESULT put_DiscretionaryAcl
([in] IDispatch *pIDispDACL);

 Discretionary access-control list that specifies types of access allowed to the object for specified users and groups.
DaclDefaulted

[Visual Basic]
Access: Read/Write
Data Type: VARIANT_BOOL

[C++]
HRESULT get_DaclDefaulted
([out] VARIANT_BOOL *fDaclDefaulted);

HRESULT put_DaclDefaulted
([out] VARIANT_BOOL fDaclDefaulted);

 A flag of the BOOL type to indicate if the DACL is derived from a default mechanism, rather than by the original provider of the security descriptor explicitly. For example, if an object's creator does not specify a DACL, the object receives the default DACL from the creator's access token. This flag can affect how the system treats the DACL, with respect to ACE inheritance. The system ignores this flag if the SE_DACL_PRESENT flag is not set.
SystemAcl

[Visual Basic]
Access: Read/Write
Data Type: IDISPATCH

[C++]
HRESULT get_SystemAcl
([out] IDispatch **ppIDispSACL);

HRESULT put_SystemAcl
([in] IDispatch *pIDispSACL);

 System access-control list used to generate audit records for the object.
SaclDefaulted

[Visual Basic]
Access: Read/Write
Data Type: VARIANT_BOOL

[C++]
HRESULT get_SaclDefaulted
([out] VARIANT_BOOL *fSaclDefaulted);

HRESULT put_SaclDefaulted
([out] VARIANT_BOOL fSaclDefaulted);

 A flag of the BOOL type to indicate if the SACL is derived from a default mechanism, rather than by the original provider of the security descriptor explicitly. This flag can affect how the system treats the SACL, with respect to ACE inheritance. The system ignores this flag if the SE_SACL_PRESENT flag is not set.

Example Code [Visual Basic]

Dim ou As IADs
Dim sd As IADsSecurityDescriptor
Dim dacl As IADsAccessControlList
Dim sacl As IADsAccessControlList

Set ou = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
Set sd = ou.Get("ntSecurityDescriptor")
Debug.Print sd.Owner
Debug.Print sd.Group
Debug.Print sd.Owner
Debug.Print sd.Revision
Set dacl = sd.DiscretionaryAcl
Set sacl = sd.SystemAcl

Example Code [C++]

For C++ code snippet demonstrating how to work the property methods of a security descriptor, see the introductory discussion of the IADsSecurityDescriptor interface.

See Also

IADsAccessControlEntry, IADsAccessControlList, Using Credentials and Security


IADsSecurityDescriptor::CopySecurityDescriptor

The IADsSecurityDescriptor::CopySecurityDescriptor method copies an ADSI security descriptor object that holds security information about an object.

HRESULT CopySecurityDescriptor(
  IDispatch ** ppSecurityDescriptor
);

Parameters

[out] ppSecurityDescriptor
Pointer to a security descriptor object.

Return Values

This method returns the standard return values, including E_INVALIDARG, E_OUTOFMEMORY, E_UNEXPECTED, and E_FAIL, as well as S_OK. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

Dim sd As IADsSecurityDescriptor
Dim sdNew As IADsSecurityDescriptor

Set ou = GetObject("LDAP://OU=Sales, DC=ArcadiaBay,DC=com")
Set sd = ou.Get("ntSecurityDescriptor")
Set sdNew = sd.CopySecurityDescriptor
ou.Put "ouSD", sdNew
ou.SetInfo

Example Code [C++]

The following C++ code snippet creates a private security descriptor by copying from an existing one, using IADsSecurityDescriptor::CopySecurityDescriptor.

IADsSecurityDescriptor *getSD(IADs*); // See IADsSecurityDescriptor

HRESULT copySD(IADs *pADsFrom, IADs *pADsTo)
{
   IADsSecurityDescriptor *psdFrom = NULL;

   // get the security descriptor from the source object
   psdFrom = getSD(pADsFrom);
   if(!psdFrom) return E_FAIL;

   // copy the source SD to the destination object.
   IDispatch *pDisp=NULL;
   HRESULT hr;
   hr = psdFrom->CopySecurityDescriptor(&pDisp);
   if(FAILED(hr)) {
      psd->Release();
      return hr;
   }

   VARIANT var;
   VariantInit(&var);
   V_DISPATCH(&var)=pDisp;
   V_VT(&var)=VT_DISPATCH;

   pADsTo->Put(L"privateSD",var);
   VariantClear(&var);
   if(psdFrom) psdFrom->Release();

   // commit the change to the directory
   pADsTo->SetInfo();
   return S_OK;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

IADsAccessControlEntry, IADsAccessControlList, Using Credentials and Security


Non-Automation Interfaces

This section describes the following non-Automation ADSI interfaces:

  • IDirectoryObject
  • IDirectorySearch


IDirectoryObject

The IDirectoryObject interface provides non-Automation clients with direct access to directory service objects. The interface enables access by means of a direct on-the-wire protocol, rather than through the ADSI property cache. Using the on-the-wire protocol helps to optimize performance. In place of the properties supported by the IADs interface, IDirectoryObject provides methods that support a subset of an object's properties and provide access to its attributes. With IDirectoryObject, a client can get or set any number of object attributes with one method call. Unlike the corresponding Automation methods, which are invoked in batch, those of IDirectoryObject are executed as soon as they are called. Properties are not cached when this interface handles them.

Non-Automation clients can call the methods of IDirectoryObject to optimize performance and take full advantage of native directory service interfaces. Automation clients cannot use IDirectoryObject. Instead, they should use the IADs interface.

Of the ADSI system-supplied providers, only the LDAP provider supports this interface.

The IDirectoryObject interface is a pure COM interface and exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDirectoryObject methods | Description
GetObjectInformation | Gets information about a directory service object.
GetObjectAttributes | Gets one or more attributes of a directory service object.
SetObjectAttributes | Sets one or more attributes of a directory service object.
CreateDSObject | Creates a directory service object.
DeleteDSObject | Deletes a directory service object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectoryObject::CreateDSObject

The IDirectoryObject::CreateDSObject method creates a child of the current directory service object.

HRESULT CreateDSObject(
  LPWSTR pszRDNName,
  PADS_ATTR_INFO pAttributeEntries,
  DWORD dwNumAttributes,
  IDispatch **ppObject
);

Parameters

pszRDNName
[in] Provides the relative distinguished name (relative path) of the object to be created.
pAttributeEntries
[in] An array of ADS_ATTR_INFO structures containing attribute definitions to be set when the object is created.
dwNumAttributes
[in] Provides a number of attributes being set when the object is created.
ppObject
[out] Provides a pointer to the IDispatch interface on the created object.

Return Values

This method returns the standard return values, including S_OK for a successful operation. For other return values, see ADSI Error Codes.

Remarks

You must specify all attributes that need to be initialized on creation in the pAttributeEntries array. You may also specify optional attributes.

Example Code [C++]

The following is a C/C++ code snippet that illustrates how to create a user object using the IDirectoryObject::CreateDSObject method.

HRESULT    hr;
IDirectoryObject *pDirObject=NULL;
ADSVALUE   sAMValue;
ADSVALUE   uPNValue;
ADSVALUE   classValue;
LPDISPATCH pDisp;

ADS_ATTR_INFO  attrInfo[] =
{
   { L"objectClass", ADS_ATTR_UPDATE,
                       ADSTYPE_CASE_IGNORE_STRING, &classValue, 1 },
   {L"sAMAccountName", ADS_ATTR_UPDATE,
                       ADSTYPE_CASE_IGNORE_STRING, &sAMValue, 1},
   {L"userPrincipalName", ADS_ATTR_UPDATE,
                      ADSTYPE_CASE_IGNORE_STRING, &uPNValue, 1},
};
DWORD dwAttrs = sizeof(attrInfo)/sizeof(ADS_ATTR_INFO);

classValue.dwType = ADSTYPE_CASE_IGNORE_STRING;
classValue.CaseIgnoreString = L"user";

sAMValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
sAMValue.CaseIgnoreString = L"mikes";

uPNValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
uPNValue.CaseIgnoreString = L"mikes@arcadiabay.com";

hr = ADsGetObject(L"LDAP://OU=Sales,DC=ArcadiaBay,DC=com",
          IID_IDirectoryObject, (void**) &pDirObject );

if ( SUCCEEDED(hr) )
{
   hr = pDirObject->CreateDSObject( L"CN=Neil Smith",  attrInfo,
                                    dwAttrs, &pDisp );
   pDisp->Release();
}
pDirObject->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectoryObject::DeleteDSObject

The IDirectoryObject::DeleteDSObject method deletes a directory service object.

HRESULT DeleteDSObject(
  LPWSTR pszRDNName
);

Parameters

pszRDNName
The relative distinguished name (relative path) of the object to be deleted.

Return Values

This method returns the standard return values, including S_OK for a successful operation. For other return values, see ADSI Error Codes.

Example Code [C++]

The following C/C++ code snippet shows how to delete a user object.

HRESULT hr;
IDirectoryObject *pDirObject=NULL;
hr = ADsGetObject(L"LDAP://OU=Sales,DC=ArcadiaBay,DC=com",
    IID_IDirectoryObject, (void**) &pDirObject );

   if ( SUCCEEDED(hr) )
   {
   hr = pDirObject->DeleteDSObject( L"CN=Neil Smith" );
   }
pDirObject->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectoryObject::GetObjectAttributes

The IDirectoryObject::GetObjectAttributes method gets one or more specified attributes of the directory service object, as defined in the ADS_ATTR_INFO structure.

HRESULT GetObjectAttributes(
  LPWSTR * pAttributeNames,
  DWORD dwNumberAttributes,
  PADS_ATTR_INFO * ppAttributeEntries,
  DWORD * pdwNumAttributesReturned
);

Parameters

pAttributeNames
[in] Specifies an array of names of the attributes that are requested. If NULL, all of the object's attributes are requested and the dwNumberAttribute parameter is assigned a value of -1.
dwNumberAttributes
[in] Specifies the size of the pAttributeNames array. If -1, all of the object's attributes are requested.
ppAttributeEntries
[out] Specifies the address of a pointer to an array of ADS_ATTR_INFO structures containing the requested attributes. If NULL, no attributes could be obtained from the directory service object.
pdwNumAttributesReturned
[out] Specifies the address of a DWORD variable that contains the number of attributes obtained by the GetObjectAttributes method.

Return Values

This method returns the standard values, as well as the following:

S_OK
The attribute definitions were obtained successfully.
E_ADS_PROPERTY_NOT_FOUND
One or more requested attributes cannot be found.

For other return values, see ADSI Error Codes.

Remarks

ADSI allocates the memory for the array of ADS_ATTR_INFO, as pointed to by ppAttributeEntries. However, the caller must call FreeADsMem to free the array. The order of attributes returned in ppAttributeEntries may not necessarily be the same as requested in pAttributeName.

Example Code [C++]

The following C++ code snippet shows how the IDirectoryObject::GetObjectAttribute method can be used.

HRESULT   hr;
ADS_ATTR_INFO   *pAttrInfo=NULL;
DWORD   dwReturn;
LPWSTR   pAttrNames[]={L"givenName",L"sn", L"otherTelephone" };
DWORD   dwNumAttr=sizeof(pAttrNames)/sizeof(LPWSTR);
IDirectoryObject   *pDirObject=NULL;

hr=ADsGetObject(L"LDAP://CN=Neil Smith,OU=Sales,DC=ArcadiaBay,DC=com",
                     IID_IDirectoryObject,
                     (void**) &pDirObject );

/////////////////////////////////////////
// Get attribute values requested
// Note: The order is not necessarily the
// same as requested using pAttrNames.
///////////////////////////////////////////
hr = pDirObject->GetObjectAttributes( pAttrNames,
                                      dwNumAttr,
                                      &pAttrInfo,
                                      &dwReturn );

if ( SUCCEEDED(hr) )
{
   for(DWORD idx=0; idx < dwReturn;idx++, pAttrInfo++ )
   {
       if ( _wcsicmp(pAttrInfo->pszAttrName,L"givenName") == 0 )
       {
           printf("First Name: %S\n",
                   pAttrInfo->pADsValues->CaseIgnoreString);
       }
       else if ( _wcsicmp(pAttrInfo->pszAttrName, L"sn") == 0 )
       {
           printf("Last Name: %S\n",
                   pAttrInfo->pADsValues->CaseIgnoreString);
       }
       else if ( _wcsicmp(pAttrInfo->pszAttrName, L"otherTelephone") == 0  )
       {   //Print out multi-valued property, "Other Telephones".
           printf("Other Telephones:");
           for (DWORD val=0; val < pAttrInfo->dwNumValues; val++, pAttrInfo->pADsValues++)
           {
                printf("  %S\n", pAttrInfo->pADsValues->CaseIgnoreString);
           }
       }
   }
}

   pDirObject->Release();
   /////////////////////////////////////////////////////////////
   // Use FreeADsMem for all memory obtained from the ADSI call.
   /////////////////////////////////////////////////////////////
   FreeADsMem( pAttrInfo );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectoryObject::GetObjectInformation

The IDirectoryObject::GetObjectInformation retrieves a pointer to an ADS_OBJECT_INFO structure that contains information about the identity and location of a directory service object.

HRESULT GetObjectInformation(
  PADS_OBJECT_INFO * ppObjInfo
);

Parameters

ppObjInfo
[out] Provides the address of a pointer to an ADS_OBJECT_INFO structure that contains information about the requested directory service object. If ppObjInfo is NULL on return, GetObjectInformation cannot obtain the information requested.

Return Values

This method returns the standard return values, including S_OK when the information is obtained successfully. For other return values, see ADSI Error Codes.

Remarks

The caller should call ADsFreeMem helper function to release the ADS_OBJECT_INFO structure that GetObjectInformation creates.

Automation clients must call IADs::GetInfo.

Example Code [C++]

The following C++ code snippet illustrates how to retrieve the object information (ADS_OBJECT_INFO) using the GetObjectInformation method of an object (m_pDirObject) that implements the IDirectoryObject interface.

ADS_OBJECT_INFO *pInfo;
HRESULT hr;

hr = m_pDirObject->GetObjectInformation(&pInfo);
if (!SUCCEEDED(hr) )
{
   return;
}

//////////////////////////
// Show the attributes
/////////////////////////

printf("RDN: %S\n", pInfo->pszRDN);
printf("ObjectDN: %S\n", pInfo->pszObjectDN);
printf("Parent DN: %S\n", pInfo->pszParentDN);
printf("Class Name: %S\n", pInfo->pszClassName);
printf("Schema DN: %S\n", pInfo->pszSchemaDN);

///////////////////////////////////////////////////////////
// Do not forget to clean up the memory using FreeADsMem!
//////////////////////////////////////////////////////////
FreeADsMem( pInfo );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectoryObject::SetObjectAttributes

The IDirectoryObject::SetObjectAttributes method modifies the information of one or more specified attributes of the directory service object, as defined in the ADS_ATTR_INFO structure.

HRESULT SetObjectAttributes(
  PADS_ATTR_INFO pAttributeEntries,
  DWORD dwNumberAttributes,
  DWORD * pdwNumAttributesModified
);

Parameters

pAttributeEntries
[in] Provides an array of attributes to be modified. Each attribute contains the name of the attribute, the operation to perform on it, and the attribute value, if applicable. For more information, see the ADS_ATTR_INFO structure.
dwNumberAttributes
[in] Provides the number of attributes to be modified. This value should correspond to the size of the pAttributeEntries array.
pdwNumAttributesModified
[out] Provides a pointer to a DWORD variable that contains the number of attributes modified by the SetObjectAttributes method.

Return Values

This method returns the standard return values, including S_OK when the attributes are set successfully.

For other return values, see ADSI Error Codes.

Remarks

In Active Directory™ (LDAP provider), the IDirectoryObject::SetObjectAttributes method is a transacted call. The attributes are either all committed or discarded. Other directory providers may not transact the call.

Example Code [C++]

The following C++ code snippet sets the "sn" attribute of a user object to the value of "William" as a case insensitive string.

HRESULT    hr;
IDirectoryObject *pDirObject=NULL;
DWORD  dwReturn;
 ADSVALUE  snValue;
 ADS_ATTR_INFO  attrInfo[] = {   {L"sn",ADS_ATTR_UPDATE,ADSTYPE_CASE_IGNORE_STRING,&snValue,1} };
DWORD dwAttrs = sizeof(attrInfo)/sizeof(ADS_ATTR_INFO);

snValue.dwType=ADSTYPE_CASE_IGNORE_STRING;
snValue.CaseIgnoreString = L"William";

hr = ADsGetObject(L"LDAP://CN=David Johnson,OU=Sales,DC=ArcadiaBay,DC=com",
        IID_IDirectoryObject,
        (void**) &pDirObject );

   if ( SUCCEEDED(hr) )
   {
    hr=pDirObject->SetObjectAttributes(attrInfo, dwAttrs, &dwReturn);
   }
pDirObject->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IDirectorySearch

The IDirectorySearch interface provides a low overhead method that non-Automation clients can use to perform queries in the underlying directory.

Of the ADSI system-supplied providers, only the LDAP provider supports this interface.

The IDirectorySearch interface is a pure COM interface and exposes the following methods.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDirectorySearch methods | Description
SetSearchPreference | Sets options for conducting a search.
ExecuteSearch | Executes an individual search.
AbandonSearch | Abandons a search already under way.
GetFirstRow | Gets the first row of the search result.
GetNextRow | Gets the next row of the search result.
GetPreviousRow | Gets the previous row of the search result.
GetNextColumnName | Gets the name of the next column of the search result.
GetColumn | Gets the item in a specified column from the current row of the search result.
FreeColumn | Frees the ADS_SEARCH_COLUMN structure created by the GetColumn method.
CloseSearchHandle | Releases the search result from memory.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::AbandonSearch

The IDirectorySearch::AbandonSearch method abandons a search that an earlier call to the ExecuteSearch method initiated.

HRESULT AbandonSearch(
  ADS_SEARCH_HANDLE hSearchHandle
);

Parameters

[in] hSearchHandle
Provides a handle to the search context.

Return Values

This method returns the standard return values, including S_OK if the first row is obtained successfully.

For other return values, see ADSI Error Codes.

Remarks

IDirectorySearch::AbandonSearch may be used if the Page_Size or Asynchronous options can be specified through IDirectorySearch::SetSearchPreference before the search is executed.

Example Code [C++]

LPWSTR pszAttr[] = { L"ADsPath", L"Name", L"samAccountName" };
ADS_SEARCH_HANDLE hSearch;
DWORD dwCount= sizeof(pszAttr)/sizeof(LPWSTR);
////////////////////////////////////////////////////////////////////
//NOTE: Assume the Page Size has been set using SetSearchPreference
// For brevity, omit error handling.
////////////////////////////////////////////////////////////////////

// Search for all users with a last name that starts with h.
hr = m_pSearch->ExecuteSearch(L"(&(objectClass=user)(sn=h*))", pszAttr, dwCount, &hSearch );
while( m_pSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
{
    // Get the samAccountName
    hr = m_pSearch->GetColumn( hSearch, pszAttr[2], &col );
    if ( !SUCCEEDED(hr) )
    {
        hr = m_pSearch->AbandonSearch( hSearch );
        hr = m_pSearch->CloseSearchHandle(hSearch);
        m_pSearch->Release();
        break;
    }
   printf("%S\n", col.pADsValues->CaseIgnoreString);
   m_pSearch->FreeColumn( &col );
}

m_pSearch->CloseSearchHandle( hSearch );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::CloseSearchHandle

The IDirectory::CloseSearchHandle method closes the handle to a search result and frees the associated memory.

HRESULT CloseSearchHandle(
  ADS_SEARCH_HANDLE hSearchHandle
);

Parameters

[in] hSearchHandle
Provides a handle to the search result to be closed.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The search result was closed.
E_ADS_BAD_PARAMETER
The search handle is invalid.

For other return values, see ADSI Error Codes.

Remarks

The process that implements the IDirectorySearch::CloseSearchHandle method must also be responsible for freeing all memory allocated by the IDirectorySearch::ExecuteSearch method, including the search result and the search result handle.

The caller may call this method only once for each opened search handle and must use the IDirectorySearch::ExecuteSearch method to obtain a new search handle after issuing IDirectorySearch::CloseSearchHandle.

Example Code [C++]

ADS_SEARCH_HANDLE hSearch;
HRESULT hr;
hr = m_pSearch->ExecuteSearch(L"(&(objectCategory=user)(l=Redmond))", pszAttr, dwCount, &hSearch );
if ( SUCCEEDED(hr) )
{
   // Omit getting the data
   m_pSearch->CloseSearchHandle(hSearch);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::ExecuteSearch

The IDirectorySearch::ExecuteSearch method executes a search and passes the results to the caller. Some providers, such as LDAP, will defer the actual execution until the caller invokes the IDirectorySearch::GetFirstRow method or the IDirectorySearch::GetNextRow method.

HRESULT ExecuteSearch(
  LPWSTR pszSearchFilter,
  LPWSTR pAttributeNames,
  DWORD dwNumberAttributes,
  PADS_SEARCH_HANDLE phSearchHandle
);

Parameters

pszSearchFilter
[in] Provides a search filter string in LDAP format.
pAttributeNames
[in] Provides an array of attribute names for which data is requested. If NULL, all attributes are requested and dwNumberAttributes must be -1.
dwNumberAttributes
[in] Provides the size of the pAttributeNames array. If -1, all attributes are requested and pAttributeNames must be NULL.
phSearchHandle
[out] Provides the address of a method-allocated handle to the search context. The caller passes this handle to other methods of IDirectorySearch to examine the search result. If NULL, the search could not be executed.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The search was successfully executed.
E_ADS_BAD_PARAMETER
The search handle is invalid.

For other return values, see ADSI Error Codes.

Remarks

The caller is responsible for calling IDirectorySearch::CloseSearchHandle to release the memory allocated for the search handle and the result.

Example Code [C++]

The following C++ code snippet illustrates how to invoke IDirectorySearch::ExecuteSearch.

LPWSTR pszAttr[] = { L"ADsPath", L"Name", L"samAccountName" };
ADS_SEARCH_HANDLE hSearch;
DWORD dwCount= sizeof(pszAttr)/sizeof(LPWSTR);

// Search for all users with a last name that starts with h.
hr = m_pSearch->ExecuteSearch(L"(&(objectClass=user)(sn=h*))", pszAttr, dwCount, &hSearch );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::FreeColumn

The IDirectorySearch::FreeColumn method releases memory that the IDirectorySearch::GetColumn method allocated for data for the column.

HRESULT FreeColumn(
  PADS_SEARCH_COLUMN pSearchColumn
);

Parameters

pSearchColumn
[in] Provides a pointer to the column to be freed.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The column was freed successfully.
E_ADS_BAD_PARAMETER
The specified column is invalid or contains invalid attributes.

For other return values, see ADSI Error Codes.

Example Code [C++]

ADS_SEARCH_COLUMN col;
/*.. omit set preference and execute … */
while( m_pSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
{
   // Get the Name and display it in the list.
    hr = m_pSearch->GetColumn( hSearch, pszAttr[0], &col );
    if ( SUCCEEDED(hr) )
    {
        printf("%S\n", col.pADsValues->CaseIgnoreString);
        m_pSearch->FreeColumn( &col );
    }
}
m_pSearch->CloseSearchHandle( hSearch );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::GetColumn

The IDirectorySearch::GetColumn method gets data from a named column of the search result.

HRESULT GetColumn(
  ADS_SEARCH_HANDLE hSearchHandle,
  LPWSTR szColumnName,
  PADS_SEARCH_COLUMN pSearchColumn
);

Parameters

hSearchHandle
[in] Provides a handle to the search context.
szColumnName
[in] Provides the name of the column for which data is requested.
pSearchColumn
[out] Provides the address of a method-allocated ADS_SEARCH_COLUMN structure that contains the column from the current row of the search result.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The column was obtained successfully.
E_ADS_BAD_PARAMETER
The search handle or the column is invalid.
E_ADS_COLUMN_NOT_SET
No attributes and values can be found for the column.

For other return values, see ADSI Error Codes.

Remarks

The method allocates the memory for the ADS_SEARCH_COLUMN structure to hold the data of the column. But the caller is responsible for freeing the memory by calling IDirectorySearch::FreeColumn.

Example Code [C++]

ADS_SEARCH_COLUMN col;
/*.. Omit the set preference and execute*/
while( m_pSearch->GetNextRow( hSearch) != S_ADS_NOMORE_ROWS )
{
   // Get the Name and display it in the list.
   hr = m_pSearch->GetColumn( hSearch, pszAttr[0], &col );
   if ( SUCCEEDED(hr) )
   {
       printf("%S\n", col.pADsValues->CaseIgnoreString);
       m_pSearch->FreeColumn( &col );
   }
}
m_pSearch->CloseSearchHandle( hSearch );

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

ADS_SEARCH_COLUMN, Using Directory Service Queries


IDirectorySearch::GetFirstRow

The IDirectorySearch::GetFirstRow method gets the first row of a search result. This method will issue or re-issue a new search, even if you have called this method before.

HRESULT GetFirstRow(
  ADS_SEARCH_HANDLE hSearchHandle
);

Parameters

hSearchHandle
[in] Provides a handle to the search context.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The first row was successfully obtained.
E_ADS_BAD_PARAMETER
The search handle is invalid.
S_ADS_NOMORE_ROWS
No rows can be found.

For other return values, see ADSI Error Codes.

Remarks

When the ADS_SEARCHPREF_CACHE_RESULTS flag is not set (that is, FALSE), only forward scrolling is permitted, because the client might not cache all the query results. Calling IDirectorySearch::GetFirstRow more than once from the same row requires some back-scrolling and could result in erroneous outcomes for a paged or an asynchronous search initiated through OLE DB when the results are not guaranteed to remain in the cache.

Example Code [C++]

hr = m_pSearch->ExecuteSearch(L"(objectCategory=contact)", pszAttr, dwCount, &hSearch );
if ( SUCCEEDED(hr) )
{
    hr = m_pSearch->GetFirstRow(hSearch);
      /* Get the data */
    m_pSearch->CloseSearchHandle(hSearch);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::GetNextColumnName

The IDirectorySearch::GetNextColumnName method gets the name of the next column in the search result that contains data.

HRESULT GetNextColumnName(
  ADS_SEARCH_HANDLE hSearchHandle,
  LPWSTR * ppszColumnName
);

Parameters

hSearchHandle
[in] Provides a handle to the search context.
ppszColumnName
[out] Provides the address of a pointer to a method-allocated string containing the requested column name. If NULL, no subsequent rows contain data.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The name of the next column containing data was successfully obtained.
E_ADS_BAD_PARAMETER
The search handle is invalid.
S_ADS_NOMORE_COLUMNS
The current column is the last column and remains unchanged.

For other return values, see ADSI Error Codes.

Remarks

This method allocates sufficient memory for the column name, but the caller must call the FreeADsMem helper function to free this memory when it is no longer needed.

Example Code [C++]

LPWSTR pszColumn;
m_pSearch->GetFirstRow( hSearch );
printf("Column names are: ");
while( m_pSearch->GetNextColumnName( hSearch, &pszColumn ) != S_ADS_NOMORE_COLUMNS )
{
   printf("%S ", pszColumn );
   FreeADsMem( pszColumn );
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::GetNextRow

The IDirectorySearch::GetNextRow method gets the next row of the search result. If IDirectorySearch::GetFirstRow is never called before, the IDirectorySearch::GetNextRow will issue a new search beginning from the first row. Otherwise, this method will advance to the next row.

HRESULT GetNextRow(
  ADS_SEARCH_HANDLE hSearchHandle
);

Parameters

hSearchHandle
[in] Provides a handle to the search context.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The next row was successfully obtained.
E_ADS_BAD_PARAMETER
The search handle is invalid.
S_ADS_NOMORE_ROWS
The current row is the last row and remains to be unchanged.

For other return values, see ADSI Error Codes.

Remarks

When the ADS_SEARCHPREF_CACHE_RESULTS flag is not set, only forward scrolling is permitted, because the client might not cache all the query results. Example Code [C++]

hr = m_pSearch->ExecuteSearch(L"(objectCategory=contact)", pszAttr, dwCount, &hSearch );
if ( SUCCEEDED(hr) )
{
   while(  m_pSearch->GetNextRow(hSearch)  != S_ADS_NOMORE_ROWS )
   {
      /* Get the data */
   }
   m_pSearch->CloseSearchHandle(hSearch);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::GetPreviousRow

The IDirectorySearch::GetPreviousRow method gets the previous row of the search result. If the provider does not provide cursor support, it should return E_NOTIMPL.

HRESULT GetPreviousRow(
  ADS_SEARCH_HANDLE hSearchHandle
);

Parameters

hSearchHandle
[in] Provides a handle to the search context.

Return Values

This method returns the standard return values, as well as the following:

S_OK
The previous row was successfully obtained.
E_NOTIMPL
The provider does not provide the cursor support for search.

For other return values, see ADSI Error Codes.

Remarks

When the ADS_SEARCHPREF_CACHE_RESULTS flag is not set, only forward scrolling is permitted, because the client might not cache all the query results.

Example Code [C++]

hr = m_pSearch->ExecuteSearch(L"(&(objectCategory=user)(st=WA))", pszAttr, dwCount, &hSearch );
if ( SUCCEEDED(hr) )
{
   while(  m_pSearch->GetNextRow(hSearch)  != S_ADS_NOMORE_ROWS )
   {
      /* Get the data */
   }
   // Print it backward
   hr = m_pSearch->GetPreviousRow( hSearch );
   while( hr != S_ADS_NOMORE_ROWS  &&  hr != E_NOTIMPL)
   {
      /* Get the data */
   }
   m_pSearch->CloseSearchHandle(hSearch);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

Using Directory Service Queries


IDirectorySearch::SetSearchPreference

The IDirectorySearch::SetSearchPreference method specifies a search preference for obtaining data in a subsequent search.

HRESULT SetSearchPreference(
  PADS_SEARCHPREF_INFO pSearchPrefs,
  DWORD dwNumPrefs
);

Parameters

pSearchPrefs
[in] Provides a caller-allocated array of ADS_SEARCHPREF_INFO structures that contain the search preferences to be set.
dwNumPrefs
[in] Provides the size of the pSearchPrefs array.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The search preference was set.
E_ADS_BAD_PARAMETER
The caller has supplied an invalid array of the ADS_SEARCHPREF_INFO structures.

For other return values, see ADSI Error Codes.

Example Code [C++]

The following C++ code snippet illustrates how to set the page size preference:

ADS_SEARCHPREF_INFO prefInfo[1];
prefInfo[0].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
prefInfo[0].vValue.dwType = ADSTYPE_INTEGER;
prefInfo[0].vValue.Integer = 100;
hr = m_pSearch->SetSearchPreference( prefInfo, 1);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.

See Also

ADS_SEARCHPREF_INFO, Using Directory Service Queries


Extension Interfaces

This section describes the IADsExtension interface and its methods.


IADsExtension

The IADsExtension interface forms the basis of the ADSI application extension model. It gives independent software vendors (ISVs) the ability to add application-specific behaviors (methods or functions) into an existing ADSI object. Multiple vendors can independently extend the functionality of the same object to meet different needs.

The extension model is based on the aggregation model in COM. An aggregator, or outer object, can add to its repertoire of methods those of an aggregate object, or inner object. An ADSI extension object, which implements the IADsExtension interface, is simply an aggregate object, whereas an ADSI provider is an aggregator.

Note When implementing an extension module, you should make sure to release an interface as soon as you are done with it. Otherwise, the aggregator can not release the interface even when its use is no longer needed.

The IADsExtension interface can be used for two purposes:

  1. The extension component needs an initialization notification as defined by ADSI_EXT_dwCode in the Operate() method. In this case, an extension client needs only to call the Operate() method. The other two methods, namely, PrivateInvoke() and PrivateGetIDsOfNames(), usually returns E_NOTIMPL in the HRESULT value.
  2. The extension component supports any dual or dispatch interface. In this case, an extension client will need to call the PrivateGetIDsOfNames() or PrivateInvoke() methods. Operate() usually ignores the data and returns E_NOTIMPL in the HRESULT value.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IADsExtension methods | Description
Operate | Performs the specified extended functionality.
PrivateGetIDsOfNames | Maps the name(s) to a DISPID.
PrivateInvoke | Invokes methods of the extended object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsExtension::Operate

The IADsExtension::Operate method is invoked by the aggregator to perform the extended functionality. The method interprets the control code and input parameters according to the specifications of the provider. Consult the provider's documentation for the proper usage.

HRESULT IADsExtension::Operate(
  DWORD dwCode,
  VARIANT varData1,
  VARIANT varData2,
  VARIANT varData3,
);

Parameters

dwCode
[in] A value of the ADSI extension control code. ADSI defines the following code value.
dwCode | Description
ADS_EXT_INITCREDENTIALS | Verifies user credentials in the extension object.

varData1
[in] Provider-supplied data the extension object will operate on. The value depends upon the control code value and is presently undefined.
varData2
[in] Provider-supplied data the extension object will operate on. The value depends upon the control code value and is presently undefined.
varData3
[in] Provider-supplied data the extension object will operate on. The value depends upon the control code value and is presently undefined.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation is not successful.
E_NOTIMPL
The method is not implemented.

For other return values, see ADSI Error Codes.

Remarks

The aggregator will ignore the E_FAIL and E_NOTIMPL return values.

Example Code [C++]

The following C/C++ code snippet shows a generic implementation.

STDMETHOD(Operate)(ULONG dwCode, VARIANT varData1, VARIANT varData2, VARIANT varData3)
{
   HRESULT hr = S_OK;
   switch (dwCode)
   {
      case ADS_EXT_INITCREDENTIALS:
      // You can prompt for a credential.
      // MessageBox(NULL, "INITCRED", "ADsExt", MB_OK);
      break;
      default:
          hr = E_FAIL;
      break;
    }
    return hr;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsExtension::PrivateGetIDsOfNames

The IADsExtension::PrivateGetIDsOfNames method is called by the aggregator, ADSI, after ADSI determines that the extension is for supporting a dual or dispatch interface. The method could use the type information to get DISPID using IDispatch::DispGetIDsOfNames.

HRESULT IADsExtension::PrivateGetIDsOfNames(
  REFIID riid,
  OLECHAR *rgszNames,
  UINT cNames,
  LCID lcid,
  DISPID *rgDispid
);

Parameters

riid
Reserved for future use. It must be IID_NULL.
rgszNames
Passed-in array of names to be mapped.
cNames
Count of the names to be mapped.
lcid
The locale context in which to interpret the names.
rgDispid
Caller-allocated array, each element of which contains an identifier (ID) corresponding to one of the names passed in the rgszNames array. The first element represents the member name. The subsequent elements represent each of the member's parameters.

Return Values

The return values are the same as those of the standard IDispatch::GetIDsOfNames method. For other return values, see ADSI Error Codes.

Remarks

All the parameters have the same meaning as the corresponding ones in the standard IDispatch::GetIDsOfNames(). The extension component returns a unique identifier (rgDispID) for each method or property defined in the supported dual interfaces. The uniqueness is enforced within the extension component. The ADSI provider is responsible for ensuring the uniqueness of the DISPIDs of all extension objects and the aggregator (ADSI) itself. The rgDispID parameter must be between 1 and 16777215 (2^24-1), or -1 (DISPID_UNKNOWN).

Example Code [C++]

The following C/C++ code snippet shows a generic implementation of this method.

STDMETHOD(PrivateGetIDsOfNames)(REFIID riid, OLECHAR ** rgszNames, unsigned int cNames, LCID lcid, DISPID  * rgdispid)
{
  if (rgdispid == NULL)
  {
     return E_POINTER;
  }
  return  DispGetIDsOfNames(m_pTypeInfo, rgszNames, cNames, rgdispid);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsExtension::PrivateInvoke

The IADsExtension::PrivateInvoke method is normally called by ADSI after the IADsExtension::PrivateGetIDsOfNames method. This method can either have a custom implementation or it can delegate the operation to IDispatch::DispInvoke method.

HRESULT IADsExtension::PrivateInvoke(
  DISPID dispidMember,
  REFIID riid,
  LCID lcid,
  WORD wFlags,
  DISPPARAMS *pDispParams,
  VARIANT *pvarResult,
  EXCEPINFO *pExcepInfo,
  UINT *puArgErr
);

Parameters

dispidMember
Identifies the member. Use the IADsExtension::PrivateGetIDsOfNames method to obtain the dispatch identifier.
riid
Reserved for future use. Must be IID_NULL.
lcid
The locale context in which to interpret arguments. The IADsExtension::PrivateGetIDsOfNames function uses lcid. It is also passed to the PrivateInvoke method to allow the object to interpret the arguments that are specific to a locale.
wFlags
Flags describing the context of the PrivateInvoke call, include:
Value | Description
DISPATCH_METHOD | The member is invoked as a method. If a property has the same name, both this and the DISPATCH_PROPERTYGET flag may be set.
DISPATCH_PROPERTYGET | The member is retrieved as a property or data member.
DISPATCH_PROPERTYPUT | The member is changed as a property or data member.
DISPATCH_PROPERTYPUTREF | The member is changed by a reference assignment, rather than a value assignment. This flag is valid only when the property accepts a reference to an object.

pDispParams
Pointer to a structure that contains an array of arguments, an array of argument DISPIDs for named arguments, and counts for the number of elements in the arrays.
pvarResult
Pointer to the location where the result is to be stored, or NULL if the caller expects no result. This argument is ignored if DISPATCH_PROPERTYPUT or DISPATCH_PROPERTYPUTREF is specified.
pExcepInfo
Pointer to a structure that contains exception information. This structure should be filled in if DISP_E_EXCEPTION is returned. Can be NULL.
puArgErr
The index within rgvarg for the first argument that has an error. Arguments are stored in pDispParams->rgvarg in reverse order, so the first argument is the one with the highest index in the array. This parameter is returned only when the resulting return value is DISP_E_TYPEMISMATCH or DISP_E_PARAMNOTFOUND.

Return Values

This method supports the standard return values, as well as the following:

S_OK
Success.
DISP_E_BADPARAMCOUNT
The number of elements provided to DISPPARAMS is different from the number of arguments that the method or property accepts.
DISP_E_BADVARTYPE
One of the arguments in rgvarg is not a valid variant type.
DISP_E_EXCEPTION
The application needs to raise an exception. In this case, the structure passed in pExcepInfo should be filled in.
DISP_E_MEMBERNOTFOUND
The requested member does not exist, or the call to PrivateInvoke tried to set the value of a read-only property.
DISP_E_NONAMEDARGS
This implementation does not support named arguments.
DISP_E_OVERFLOW
One of the arguments in rgvarg could not be coerced to the specified type.
DISP_E_PARAMNOTFOUND
One of the parameter DISPIDs does not correspond to a parameter on the method. In this case, puArgErr should be set to the first argument that contains the error.
DISP_E_TYPEMISMATCH
One or more of the arguments could not be coerced. The index within rgvarg of the first parameter with the incorrect type is returned in the puArgErr parameter.
DISP_E_UNKNOWNINTERFACE
The interface identifier passed in riid is not IID_NULL.
DISP_E_UNKNOWNLCID
The member being invoked interprets string arguments according to the LCID, and the LCID is not recognized. If the LCID is not needed to interpret arguments, this error should not be returned.
DISP_E_PARAMNOTOPTIONAL
A required parameter was omitted.

For other return values, see ADSI Error Codes.

Example Code [C++]

The following implementation of IADsExtension::PrivateInvoke delegates the operation to the DispInvoke() method.

STDMETHOD(PrivateInvoke)(
   DISPID dispidMember,
   REFIID riid,
   LCID lcid,
   WORD wFlags,
   DISPPARAMS * pdispparams,
   VARIANT * pvarResult,
   EXCEPINFO * pexcepinfo,
   UINT * puArgErr)
{
   return DispInvoke((IHelloWorld*)this,
   m_pTypeInfo,
   dispidMember,
   wFlags,
   pdispparams,
   pvarResult,
   pexcepinfo,
   puArgErr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


Utility Interfaces

This section describes the following utility interfaces:

  • IADsDeleteOps
  • IADsObjectOptions
  • IADsPathname


IADsDeleteOps

The IADsDeleteOps interface specifies a single method that an object can use to delete itself from the underlying directory. For a container object, the method also deletes all its children and the children's children, i.e., the whole subtree.

The interface is designed to offer the functionality that complements that provided by the IADsContainer interface. To remove an object from the directory store, you can request its parent object to do the job. That amounts to calling the IADsContainer::Delete method on the contained object. When the object also implements the IADsDeleteOps interface, you can ask the object to remove itself (and all the contained objects) by calling the IADsDeleteOps::DeleteObject method directly on the object.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of the object's programmable interface.
GetIDsOfNames | Maps the name of the method or property to the DISPID.
Invoke | Calls one of the object's methods, or gets and sets one of its properties.

IADsDeleteOps methods | Description
DeleteObject | Deletes the object from the directory.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsDeleteOps::DeleteObject

The IADsDeleteOps::DeleteObject method deletes an ADSI object.

HRESULT DeleteObject(
  Long lnFlags
);

Parameters

lnFlags
Reserved for future use.

Return Values

This method supports the standard return values, including S_OK for a successful operation. For error code information, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to delete an object and all its children using this method.

Dim cont As IADsContainer
Dim ops As IADsDeleteOps

Set cont = GetObject("LDAP://OU=sales,DC=ArcadiaBay,DC=com")
Set ops = cont

ops.DeleteObject (0)

Example Code [C++]

The following C++ code snippet deletes an object and all its children.

IADsContainer *pCont;
LPWSTR adsPath = L"LDAP://OU=Sales,DC=ArcadiaBay,DC=com";
HRESULT hr = ADsGetObject(adsPath,
                          IID_IADsContainer,
                          (void**)&pCont);
if (FAILED(hr)) exit(hr);

IADsDeleteOps *pOps;
hr = pCont->QueryInterface(IID_IADsDeleteOps,(void**)&pOps);
pCont->Release();
if(FAILED(hr) exit(hr);

pOps->DeleteObject(0);
pOps-Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsObjectOptions

The IADsObjectOptions interface provides a direct mechanism to specify and obtain provider-specific options for manipulating an ADSI object. Typically, the options apply to search operations of the underlying directory store. The supported options are provider-specific.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of the object's programmable interface.
GetIDsOfNames | Maps the name of the method or property to the DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsObjectOptions methods | Description
GetOption | Get the option
SetOption | Set the option

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsObjectOptions::GetOption

The IADsOptions::GetOption method retrieves a provider-specific option for manipulating a directory object.

HRESULT IADsObjectOptions::GetOption(
  DWORD dwOption,
  void *pValue)
);

Parameters

dwOption
[in] A constant that indicates a provider-specific option, as defined in the ADS_OPTION_ENUM enumeration.
pValue
[retval, out] A pointer to a setting of the option specified by dwOption. For example, if dwOption is set to be ADS_OPTION_PAGE_SIZE, pValue refers to the number of pages.

Return Values

The method supports the standard return values, including S_OK if the operation is successful, and E_ADS_BAD_PARAMETER if the user has supplied an invalid pValue parameter. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code example shows how to search for the host name of a server using the ADS_OPTION_SERVERNAME option, and determine the page size using the ADS_OPTION_PAGE_SIZE option.

Dim cont As IADsContainer
Dim opt As IADsObjectOptions

Set cont = GetObject("LDAP://OU=Sales,DC=ArcadiaBay,DC=com")
Set opt = cont
srvName = opt.GetOption(ADS_OPTION_SERVERNAME)
MsgBox "We connecting to " & srvName
PageSize = opt.GetOption(ADS_OPTION_PAGE_SIZE)

Example Code [C++]

The following C++ code snippet searches for the host name of a server and determine the page size using the ADS_OPTION_SERVERNAME and ADS_OPTION_PAGE_SIZE options, respectively.

IADsContainer *pCont;
IADsObjectOptions *pOps;
LPWSTR adsPath = L"LDAP://OU=Sales,DC=ArcadiaBay,DC=com";
HRESULT hr =S_OK;
hr = ADsGetObject(adsPath,IID_IADsContainer,(void**)&pCont);
if(FAILED(hr)) exit(hr);

hr = pCont->QueryInterface(IID_IADsObjectOptions,(void**)&pOps);
pCont->Release();

VARIANT var;
VariantInit(&var);
hr = pOps->GetOptions(ADS_OPTION_SERVERNAME,&var);
printf("Server name: %n", V_BSTR(&var));
VariantClear(&var);

hr = pOps->GetOption(ADS_OPTION_PAGE_SIZE, &var);
printf("Page size : %d\n",V_I4(&var));

VariantClear(&var);
pOps->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsObjectOptions::SetOption

The IADsOptions::SetOption method sets the provider-specific options for manipulating a directory object.

HRESULT IADsObjectOptions::SetOption(
  DWORD dwOption,
  void *pValue)
);

Parameters

dwOption
This parameter is a DWORD value that specifies the options of an object.
pValue
This parameter provides the specific setting for a given option.

Return Values

The method supports the standard return values, including S_OK for a successful operation and E_ADS_BAD_PARAMETER when the user has supplied an invalid pValue parameter. For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to set options on a container object to enable paged search and referral chasing.

Const ADS_CHASE_REFERRALS_SUBORDINATE = &H20
Dim cont As IADsContainer
Dim chaseRef As Variant
Dim opt As IADsObjectOptions

Set cont = GetObject("LDAP://DC=Sales,DC=ArcadiaBay,DC=com")
Set opt = cont

'Setting the referral and page size and then enumerate all the children objects.
chaseRef = ADS_CHASE_REFERRALS_SUBORDINATE
PageSize = 100

opt.SetOption ADS_OPTION_REFERRALS, chaseRef
opt.SetOption ADS_OPTION_PAGE_SIZE, PageSize
For Each child In cont
  Debug.Print child.Name
Next

Example Code [C++]

The following C++ code snippet sets options on a container object to enable paged search and referral chasing.

IADsContainer *pCont;
IADsObjectOptions *pOps;
LPWSTR adsPath = L"LDAP://OU=Sales,DC=ArcadiaBay,DC=com";
HRESULT hr =S_OK;
hr = ADsGetObject(adsPath,IID_IADsContainer,(void**)&pCont);
if(FAILED(hr)) exit(hr);

hr = pCont->QueryInterface(IID_IADsObjectOptions,(void**)&pOps);
pCont->Release();

VARIANT var;
VariantInit(&var);
V_I4(&var)=ADS_CHASE_REFERRALS_SUBORDINATE;
V_VT(&var)=VT_I4;
hr = pOps->SetOption(ADS_OPTION_REFERRALS,var);

VariantClear(&var);
V_I4(&var)=100;
V_VT(&var)=VT_I4;
hr = pOps->SetOption(ADS_OPTION_PAGE_SIZE, var);

VariantClear(&var);
pOps->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname

The IADsPathname interface is designed for parsing the X.500 and Windows paths in ADSI. You can use this interface to do the following:

  • Set and retrieve paths of ADSI objects in different formats
  • Extract or add each element for a given ADsPath
  • Construct ADsPaths to be used in queries of directory objects

The IADsPathname interface is implemented on a "Pathname" object. You need to instantiate the "Pathname" object in order to use the methods defined in the IADsPathname interface. This means that you need to call explicitly the CoCreateInstance() function in C++ or the New operator in Visual Basic. See the code snippets given at the bottom of this page.

The IADsPathname interface exposes following methods and uses two enumeration types: ADS_SETTYPE, ADS_FORMAT.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsPathname methods | Description
get_EscapedMode | Retrieves the mode for escaping a path.
put_EscapedMode | Specifies the mode for escaping a path.
Set | Sets an object path with an ADS_SETTYPE option.
SetDisplayType | Specifies how a path is to be displayed.
Retrieve | Retrieves an object path with an ADS_FORMAT type.
GetNumElements | Gets the number of elements in the path.
GetElement | Gets elements stored in the object with its index.
GetEscapedElement | Escapes an RDN string and returns the output.
AddLeafElement | Adds an element to the end of the path.
RemoveLeafElement | Removes the last element from the object.
CopyPath | Generate an object with the same path.

Example Code [Visual Basic]

The following Visual Basic code snippets shows how to create an ADsPathname object.

Dim x as New Pathname
x.Set "LDAP", ADS_SETTYPE_PROVIDER

or

x.Set "LDAP://server/dc=domain1,dc=myCompany,dc=com", ADS_SETTYPE_FULL
MsgBox "Path in Windows Format: " & x.Retrieve(ADS_FORMAT_WINDOWS)

Example Code [C++]

The following C++ code snippet shows how to create an ADsPathname object.

IADsPathname *GetPathnameObject(LPWSTR adsPath)
{
   if(!adsPath) return NULL;

   IADsPathname *pPathname=NULL;
   HRESULT hr;

   hr = CoCreateInstance(CLSID_Pathname,
                         NULL,
                         CLSCTX_INPROC_SERVER,
                         IID_IADsPathname,
                         (void**)&pPathname);

   if(FAILED(hr)) {
       if(pPathname) pPathname->Release();
       return NULL;
   }

   pPathname->AddRef();
   hr = pPathname->Set(adsPath,ADS_SETTYPE_FULL);

   return pPathname;
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname Property Methods

The property methods of the IADsPathname interface get or set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property | Description
EscapedMode Access: Read/Write Data type: LONG | Examine or specify how escaped characters are handled in a path name. The options is defined in ADS_ESCAPE_MODE_ENUM.

Remarks

EscapedMode represents a state. You can turn it on or off, by setting it to ADS_ESCAPEDMODE_ON or ADS_ESCAPEDMODE_OFF, at any time. Once it is turned on (or off), all subsequent retrievals produces escaped (or unescaped) paths.

In ADSI only the IADsPathname is capable of unescaping paths. All other ADSI interfaces always return escaped ones. The default state of EscapedMode is ADS_ESCAPEDMODE_ON.

Depending upon how an ADsPath has been set, ADSI applies escaping and unescaping, to the "/" character. If the ADsPath is set using ADS_SETTYPE_FULL, ADSI will unescape any "/" charaters that have been explicitly escaped when EscapedMode is off. If the path is set using ADS_SETTYPE_DN, ADSI will escape all the "/" characters in the distinguished name when EscapedMode is on and will unescape all the "/" characters when EscapedMode is off.

At present ADSI will not touch any other LDAP special characters, for example, "(",")" "(", ")", or "*". That is, they are neither escaped nor unescaped.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to work with the EscapedMode property.

Dim path As New Pathname
path.Set "CN=foo/bar\/*", ADS_SETTYPE_DN

path.EscapedMode = ADS_ESCAPEDMODE_ON
MsgBox path.Retrieve(ADS_FORMAT_WINDOWS)
                                  ' produces "LDAP://CN=foo\/bar\/*"

path.EscapedMode = ADS_ESCAPEMODE_OFF
MsgBox path.Retrieve(ADS_FORMAT_WINDOWS)
                                  ' produces "LDAP:://CN=foo/bar/*"

path.EscapedMode = ADS_ESCAPEDMODE_DEFAULT
MsgBox path.Retrieve(ADS_FORMAT_WINDOWS)
                                  ' produces "LDAP://CN=foo\/bar\/*"

path.Set "LDAP://CN=foo/bar\/*", ADS_SETTYPE_FULL

path.EscapedMode = ADS_ESCAPEDMODE_ON
MsgBox path.Retrieve(ADS_FORMAT_WINDOWS)
                                  ' produces "LDAP://CN=foo/bar\/*"

path.EscapedMode = ADS_ESCAPEMODE_OFF
MsgBox path.Retrieve(ADS_FORMAT_WINDOWS)
                                  ' produces "LDAP://CN=foo/bar/*"

Example Code [C++]

The following C++ code snippet shows how to work with the EscapedMode property. For brevity, error checking is ignored.

   IADsPathname *pPathname=NULL;
   HRESULT hr;

   hr = CoCreateInstance(CLSID_Pathname,
                         NULL,
                         CLSCTX_INPROC_SERVER,
                         IID_IADsPathname,
                         (void**)&pPathname);

   if(FAILED(hr)) {
       if(pPathname) pPathname->Release();
       return NULL;
   }

   pPathname->AddRef();
   hr = pPathname->Set(L"LDAP://CN=foo/bar\/*",ADS_SETTYPE_FULL);

   hr = pPathname->put_EscapedMode(ADS_ESCAPEDMODE_OFF);
   hr = pPathname->Retrieve(ADS_FORMAT_DN,&bstr);
   printf("Unescaped path: %S\n",bstr);
                                // producing "LDAP://CN=foo/bar/*"
   SysFreeString(bstr);

   hr = pPathname->put_EscapedMode(ADS_ESCAPEDMODE_ON);
   hr = pPathname->Retrieve(ADS_FORMAT_DN,&bstr);
   printf("Escaped path: %S\n",bstr);
                               // producing "LDAP://CN=foo/bar\/*"
   SysFreeString(bstr);

// ////////////
//   setting the path using ADS_SETTYPE_DN

   hr = pPathname->Set(L"CN=foo/bar\/*",ADS_SETTYPE_DN);

   hr = pPathname->put_EscapedMode(ADS_ESCAPEDMODE_OFF);
   hr = pPathname->Retrieve(ADS_FORMAT_DN,&bstr);
   printf("Unescaped path: %S\n",bstr);
                                // producing "LDAP://CN=foo/bar/*"
   SysFreeString(bstr);

   hr = pPathname->put_EscapedMode(ADS_ESCAPEDMODE_ON);
   hr = pPathname->Retrieve(ADS_FORMAT_DN,&bstr);
   printf("Escaped path: %S\n",bstr);
                               // producing "LDAP://CN=foo\/bar\/*"
   SysFreeString(bstr);

   hr = pPathname->Release();


IADsPathname::Set

The IADsPathname::Set method sets the path of the object with an ADS_SETTYPE option.

HRESULT IADsPathname::Set(
  BSTR bstrADsPath,
  DWORD dwSetType
);

Parameters

bstrADsPath
[in] Path of an ADSI object.
dwSetType
[in] An ADS_SETTYPE option defining the format type to be retrieved.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_ADS_BAD_PATHNAME
The pathname is not valid.

For other return values, see ADSI Error Codes.

Remarks

This method will set the namespace as specified and identify the appropriate provider for carrying out the path cracking operation. Resetting to a different namespace will result in lose of information already set by this method.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use the Set method.

Dim x As New Pathname

x.Set "LDAP://server/CN=JSmith, DC=Microsoft, DC=Com", ADS_SETTYPE_FULL
dn = x.GetElement(0)    ' dn now is "JSmith".

Example Code [C++]

The following C++ code fragment shows how to call the IADsPathname::Set method.

   IADsPathname *pPathname=NULL;
   HRESULT hr;

   hr = CoCreateInstance(CLSID_Pathname,
                         NULL,
                         CLSCTX_INPROC_SERVER,
                         IID_IADsPathname,
                         (void**)&pPathname);

   if(FAILED(hr)) {
       if(pPathname) pPathname->Release();
       return NULL;
   }

   hr = pPathname->Set(L"LDAP://CN=foo /bar",ADS_SETTYPE_FULL);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::SetDisplayType

The IADsPathname::SetDisplayType method specifies how to display the path name of an object. It can ask the path name to be displayed in a string with both naming attributes and values, e.g., "CN=John Doe" or with values only, e.g., "John Doe".

HRESULT IADsPathname::SetDisplayType(
  long lnDisplayType
);

Parameters

lnDisplayType
The display type of a path name as defined in ADS_DISPLAY_ENUM.

Return Values

This methods supports the standard return values, including the following:

S_OK
The specified display type is valid and has been set successfully.
E_FAIL
The specified display type can not be set.

Example Code [Visual Basic]

The following Visual Basic code snippet sets to display the path name in a string with both the naming attribute and the value.

Dim x As New Pathname

x.SetDisplayType(ADS_DISPLAY_FULL)

Example Code [C++]

The following C++ code snippet sets to display the path with both the naming attributes and values. See the code snippet in IADsPathname for the GetPathnameObject function.

LPWSTR adsPath=L"LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);

pPath->SetDisplayType(ADS_DISPLAY_FULL)

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::Retrieve

The IADsPathname::Retrieve method retrieves the path of the object with different format types.

HRESULT IADsPathname::Retrieve(
  long dwFormatType,
  BSTR *pbstrADsPath
);

Parameters

dwFormatType
[in] A format type of the output path, as defined in ADS_FORMAT.
pbstrADsPath
[out] The path of the object returned.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_ADS_BAD_PATHNAME
The path set is not supported in this retrieval format.
E_INVALIDARG
The user has supplied an invalid dwFormatType parameter.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to retrieve the provider name of the "LDAP://serv1/dc=dom, dc=company,dc=com" pathname.

Dim x as New Pathname
x.Set "LDAP://serv1/dc=dom,dc=company,dc=com"
y = x.Retrieve(ADS_FORMAT_PROVIDER)    ' y now equals "LDAP".

Example Code [C++]

The following C++ code snippet shows how use IADsPathname::Retrieve method to obtain the path name string.

LPWSTR adsPath = L"WinNT://machine/aUser,user";

IADsPathname * pPath = GetPathnameObject(adsPath);
if (!pPath) return;

HRESULT hr = SetDisplayType(pPath,ADS_DISPLAY_FULL);
BSTR bstr;
hr = pPath->Retrieve(ADS_FORMAT_PROVIDER, &bstr);
printf("Provider of the path retrieved is %S\n",bstr);
SysFreeString(bstr);

pPath->Release();

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::GetNumElements

The IADsPathname::GetNumElements method gets the number of elements in the path.

HRESULT IADsPathname::GetNumElements(
  DWORD * dwNumPathElements
);

Parameters

dwNumPathElements
[out] The number of elements in the path.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to use GenNumElements.

dim x As New Pathname
x.Set = "LDAP://srv1/dc=dom,dc=company,dc=com",ADS_SETTYPE_FULL
y = x.GetNumElements()     ' y = 3.

Example Code [C++]

The following C++ code snippet prints out all the number of elements of a path. See the code snippet in IADsPathname for the GetPathnameObject function.

LPWSTR adsPath=L"LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);
long count=0;
HRESULT hr = pPath->GetNumElements(&count);
printf("Count = %d\n",count);

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::GetElement

The IADsPathname::GetElement method retrieves the path of the object with different format types.

HRESULT IADsPathname::GetElement(
  DWORD dwElementIndex,
  BSTR *pbstrElement
);

Parameters

dwElementIndex
[in] The index of the element.
pbstrElement
[out] The returned element.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
ERROR_INVALID_INDEX
The supplied index does not exist.
E_INVALIDARG
The supplied index is not valid.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to extract the first element of a pathname.

Dim x As New Pathname
x.Set "LDAP://srv1/dc=dom,dc=company,dc=com", ADS_SETTYPE_FULL
y = x.GetElement(0)            ' y becomes "dom".

Example Code [C++]

The following C++ code snippet prints out all the elements of a path. See the code snippet in IADsPathname for the GetPathnameObject function.

LPWSTR adsPath=L"LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);
long count=0;
HRESULT hr = pPath->GetNumElements(&count);
BSTR bstr;
for (int I = 0; I<count; I++) {
    hr = pPath->GetElement(I,&bstr);
    printf("Element(%d): %S\n",bstr);
    SysFreeString(bstr);
}

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::GetEscapedElement

The IADsPathname::GetEscapedElement method is used to escape special characters in the input pathname.

HRESULT IADsPathname::GetEscapedElement(
  LONG lnReserved,
  BSTR bstrInStr,
  BSTR bstrOutStr
);

Parameters

lnReserved
Reserved for future use.
bstrInStr
[in] An input string.
bstrOutStr
[out] An output string.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_NOTIMPL
The method is not supported for this particular provider.

For other return values, see ADSI Error Codes.

Remarks

This method is useful to handle pathname containing special characters as input from a user interface. It takes the input string with special characters in the RDN, escapes it, and returns the escaped string that can be recognized by a directory service.

Example Code [Visual Basic]

The following Visual Basic code snippet shows the effect produced by IADsPathname::GetEscapedElement.

Dim x As New Pathname

rdn = x.GetEscapedElement(0, "cn=smith,john")

Now, rdn becomes "cn=smith\,john".

Example Code [C++]

The following C++ code snippet shows the effect produced by IADsPathname::GetEscapedElement.

LPWSTR adsPath=L"LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);
BSTR rdn;
HRESULT hr = pPath->GetEscapedElement(0,L"cn=smith,john",&rdn);

pPath->Release();

Now rdn become "cn=smith\,john".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::AddLeafElement

The IADsPathname::AddLeafElement method adds an element to the end of the object path.

HRESULT IADsPathname::AddLeafElement(
  BSTR bstrLeafElement
);

Parameters

bstrLeafElement
[in] The name of the leaf element.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_ADS_BAD_PATHNAME
The pathname set is not supported this retrieval format.
E_INVALIDARG
The user has supplied an invalid dwFormatType parameter.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet adds the "com" element to "dc=dom,dc=company".

Dim x As New Pathname
x.Set "LDAP://srv1/dc=dom,dc=company", ADS_SETTYPE_FULL
x.AddLeafElement("dc=com")

Example Code [C++]

The following C++ code snippet shows the effect produced by calling the IADsPathname::AddLeafElement.

LPWSTR adsPath=L"LDAP://server/dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);
HRESULT hr = pPath->AddLeafElement(L"cn=jsmith");

pPath->Release();

Now the pathname becomes "LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::RemoveLeafElement

The IADsPathname::RemoveLeafElement method removes the last element from the object path.

HRESULT IADsPathname::RemoveLeafElement();

Parameters

None.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_ADS_BAD_PATHNAME
The pathname set is not supported this retrieval format.

For other return values, see ADSI Error Codes.

Example Code [Visual Basic]

The following Visual Basic code snippet shows the effect produced by calling this method.

Dim x As New Pathname
x.Set "LDAP://srv1/dc=dom,dc=company,dc=com",ADS_SETTYPE_FULL
x.RemoveLeafElement      ' now x is "LDAP://srv1/dc=company,dc=com".

Example Code [C++]

The following C++ code snippet shows the effect produced by calling the IADsPathname::RemoveLeafElement.

LPWSTR adsPath=L"LDAP://server/cn=jsmith,dc=arcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath);
HRESULT hr = pPath->RemoveLeafElement();

pPath->Release();

Now the pathname becomes "LDAP://server/dc=arcadiaBay,dc=com".

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPathname::CopyPath

The IADsPathname::CopyPath method generates an object with the same path.

HRESULT IADsPathname::CopyPath(
  IDispatch *pDisp)
);

Parameters

pDisp
[out] The IDispatch interface pointer on the returned ADsPathname object.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The operation is successful.
E_FAIL
The operation has failed.
E_INVALIDARG
The user has supplied an undefined parameter.

For other return values, see ADSI Error Codes.

Remarks

This method is useful when you want to modify the object path and retain the original object path.

Example Code [Visual Basic]

The following Visual Basic code snippet shows how to make a copy of a pathname.

Dim x, y As New Pathname
x.Set "LDAP://srv1/dc=dom,dc=company,dc=com",ADS_SETTYPE_FULL
set y = x.CopyPath
MsgBox y.Retrieve(5)

Example Code [C++]

The following C++ code snippet makes a copy of a pathname object. The code of GetPathnameObject function used below are given in IADsPathname.

IADsPathname *pPath;
LPWSTR adsPath;
adsPath = L"LDAP://server/cn=john smith,dc=ArcadiaBay,dc=com";

IADsPathname *pPath = GetPathnameObject(adsPath)
if (!pPath) exit(0);

IDispatch *pDisp;
HRESULT hr;
hr = pPath->CopyPath(&pDisp);
if(FAILED(hr)) exit(hr);

IADsPathname *pPathCopy;
hr = pDisp->QueryInterface(IID_IADsPathname,(void**)&pPathCopy);

// …

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


Data Type Interfaces

This section describes the following data type interfaces:

  • IADsAcl
  • IADsBackLink
  • IADsCaseIgnoreList
  • IADsEmail
  • IADsFaxNumber
  • IADsHold
  • IADsLargeInteger
  • IADsNetAddress
  • IADsOctetList
  • IADsPath
  • IADsPostalAddress
  • IADsReplicaPointer
  • IADsTimestamp
  • IADsTypedName


IADsAcl

The IADsAcl interface provides methods for an ADSI client to access and manipulate the ACL or Inherited ACL attribute values in Novell NetWare® Directory Services (NDS). A user uses this interface to manipulate the NDS attributes.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsAcl property methods | Description
get_ProtectedAttrName | Gets the name of a protected attribute.
put_ProtectedAttrName | Sets the name of a protected attribute.
get_SubjectName | Gets the name of a subject.
put_SubjectName | Sets the name of a subject.
get_Privileges | Gets the privilege setting.
put_Privileges | Sets the privilege setting.
CopyAcl | Make new copy of the ACL.

Remarks

For more information the ACL or Inherited ACL attribute, see Novell NetWare® Directory Services Schema Specification, version 1.1.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsAcl Property Methods

The property method of the IADsAcl interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
ProtectedAttrName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ProtecteAttrName
(BSTR *retVal);

HRESULT put_ProtectedAttrName
(BSTR bstrProtectedAttrName);

 Name of a protected attribute.
SubjectName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_SubjectName
(BSTR *retval);

HRESULT put_SubjectName
(BSTR bstrSubjectName);

 Name of the subject.
Privileges

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Privileges
(LONG *retval);

HRESULT put_Privileges
(LONG lnPrivileges);

 Privilege settings.


IADsAcl::CopyAcl

The IADsAcl::CopyAcl method makes a copy of the existing ACL.

HRESULT IADsAcl::CopyAcl(
  IDispatch *ppAcl
);

Parameters

ppAcl
Pointer to the newly created copy of the existing ACL.

Return Values

This method supports the standard return values, as well as the following:

S_OK
The copy is created successfully.

For other return values, see ADSI Error Codes.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsBackLink

The IADsBackLink interface provides methods for an ADSI client to access the Back Link attribute in Novell NetWare® Directory Services (NDS). You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsBackLink property methods | Description
get_RemoteID | Retrieves the identifier of the remote server.
put_RemoteID | Modifies the identifier of the remote server.
get_ObjectName | Retrieves the name of an object the Back Link is attached to.
put_ObjectName | Modifies the name of an object the Back Link is attached to.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsBackLink Property Methods

The property method of the IADsBackLink interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Properties Description
RemoteID

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_RemoteID
(LONG *retVal);

HRESULT put_RemoteID
(LONG bstrProtectedAttrName);

 The identifier of the remote server that requires an external reference of the object specified by ObjectName.
ObjectName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ObjectName
(BSTR *retval);

HRESULT put_ObjectName
(BSTR bstrSubjectName);

 The name of an object the Back Link attribute is attached to.

See Also

ADS_BACKLINK


IADsCaseIgnoreList

The IADsCaseIgnoreList interface provides methods for an ADSI client to access the Case Ignore List attribute in Novell NetWare® Directory Services (NDS). You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsCaseIgnoreList property methods | Description
get/put_CaseIgnoreList | Gets and puts the Case Ignore List attribute of NDS.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsCaseIgnoreList Property Methods

The property method of the IADsCaseIgnoreList interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
CaseIgnoreList

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_CaseIgnoreList
(VARIANT *retVal);

HRESULT put_CaseIgnoreList
(VARIANT vCaseIgnoreList);

 An ordered sequence of case insensitive strings.

See Also

ADS_CASEIGNORE_LIST


IADsEmail

The IADsEmail interface provides methods for an ADSI client to access the Email Address attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsEmail property methods | Description
get/put_Type | Gets/Puts the type of the Email message.
get/put_Address | Gets/Puts the Email address of the user.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsEmail Property Methods

The property method of the IADsEmail interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Type

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Type
(LONG *retVal);

HRESULT put_Type
(LONG lnType);

 The type of the email message.
Address

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Address
(BSTR *retval);

HRESULT put_Address
(BSTR bstrAddress);

 The email address of the user.

Property Description

Type
The type of the email message.
Address
The email address of the user.

See Also

ADS_EMAIL


IADsFaxNumber

The IADsFaxNumber interface provides methods for an ADSI client to access the Facsimile Telephone Number attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsFaxNumber property methods | Description
get/put_TelephoneNumber | Gets/Puts the telephone number of the fax machine.
get/put_Parameters | Gets/Puts parameters for the fax machine.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsFaxNumber Property Methods

The property method of the IADsFaxNumber interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
TelephoneNumber

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_TelephoneNumber
(BSTR *retVal);

HRESULT put_TelephoneNumber
(BSTR bstrTelephoneNumber);

 The telephone number of the fax machine.
Parameters

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Parameters
(VARIANT *retval);

HRESULT put_Parameters
(VARIANT vParameters);

 The parameters for the fax machine.

See Also

ADS_FAXNUMBER


IADsHold

The IADsHold interface provides methods for an ADSI client to access the Hold attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsHold property methods | Description
get_ObjectName | Gets the name of the object on hold.
put_ObjectName | Sets the name of the object on hold.
get_Amount | Gets the amount charged against the user for the period on hold while the server checks the account balance.
put_Amount | Sets the amount charged against the user for the period on hold while the server checks the account balance.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsHold Property Methods

The property method of the IADsHold interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
ObjectName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ObjectName
(BSTR *retVal);

HRESULT put_ObjectName
(BSTR bstrObjectName);

 The name of the object representing a user on hold.
Amount

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Amount
(LONG *retval);

HRESULT put_Amount
(LONG lnAmount);

 The amount of charges levied against the user for the period on hold while the server checks the account balance of the user.


IADsLargeInteger

The IADsLargeInteger interface provides methods to manipulate 64-bit integers of the LargeInteger type.

You can call the property methods to get and set the value of a large integer.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsLargeInteger property methods | Description
get/put_HighPart | Gets and sets the high part of the integer.
get/put_LowPart | Gets and sets the low part of the integer.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsLargeInteger Property Methods

The property methods of the IADsLargeInteger interface get and set the properties described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
HighPart

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_HighPart
(LONG *retval);

HRESULT put_HighPart
(LONG lnHighPart);

 The high part of the integer.
LowPart

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_LowPart
(LONG *retval);

HRESULT put_LowPart
(LONG lnLowPart);

 The low part of the integer.

Remarks

If largeInt is of the LargeInteger type, its value is specified by those of HighPart and LowPart according to the following formula:

largeInt = HighPart * 232 + LowPart

Example Code [Visual Basic]

The following Visual Basic® code snippet sets a large integer to 43937327281.

Dim LI as New LargeInteger
LI.HighPart = 10
LI.LowPart = 987654321


IADsNetAddress

The IADsNetAddress interface provides methods for an ADSI client to access the Net Address attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsNetAddress property methods | Description
get/put_AddressType | Gets/Puts the type of communication protocols.
get/put_Address | Gets/Puts the network address used in NDS.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsNetAddress Property Methods

The property method of the IADsNetAddress interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
AddressType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_AddressType
(LONG *retVal);

HRESULT put_AddressType
(LONG lnAddressType);

 A type of communication protocols.
Address

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_Address
(VARIANT *retval);

HRESULT put_Address
(VARIANT vAddress);

 A network address used in NDS.

See Also

ADS_NETADDRESS


IADsOctetList

The IADsOctetList interface provides methods for an ADSI client to access the Octet List attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsOctetList property methods | Description
get/put_OctetList | Gets/Puts an ordered sequence of byte arrays.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsOctetList Property Methods

The property method of the IADsOctetList interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
OctetList

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_OctetList
(VARIANT *retVal);

HRESULT put_OctetList
(VARIANT vOctetList);

 An ordered sequence of byte arrays.

See Also

ADS_OCTET_LIST


IADsPath

The IADsPath interface provides methods for an ADSI client to access the Path attribute of Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsPath property methods | Description
get/put_Type | Gets/Puts the file type of the file system.
get/put_VolumeName | Gets/Puts the name of an existing volume of the file system.
get/put_Path | Gets/Puts the path of a directory of the file system.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsPath Property Methods

The property method of the IADsPath interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
Type

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Type
(LONG *retVal);

HRESULT put_Type
(LONG lnType);

 File type of the file system.
VolumeName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_VolumeName
(BSTR *retval);

HRESULT put_VolumeName
(BSTR bstrVolumeName);

 Name of an existing volume of the file system.
Path

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_Path
(BSTR *retval);

HRESULT put_Path
(BSTR bstrPath);

 Path of a directory of the file system.

See Also

ADS_PATH


IADsPostalAddress

The IADsPostalAddress interface provides methods for an ADSI client to access the Postal Address attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsPostalAddress property methods | Description
get_PostalAddress | Retrieves the postal address.
put_PostalAddress | Modifies the postal address.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADSPostalAddress Property Methods

The property method of the IADsPostalAddress interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
PostalAddress

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_PostalAddress
(VARIANT *retVal);

HRESULT put_PostalAddress
(VARIANT vPostalAddress);

 An array of six strings holding the postal address of the user.

See Also

ADS_POSTALADDRESS


IADsReplicaPointer

The IADsReplicaPointer interface provides methods for an ADSI client to access the Replica Pointer attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsReplicaPointer property methods | Description
get/put_ServerName | Gets/Puts the server name.
get/put_ReplicaType | Gets/Puts the replica type.
get/put_ReplicaNumber | Gets/Puts the replica identifier.
get/put_Count | Gets/Puts the replica count.
get/put_ReplicaAddressHints | Gets/Puts a hints of where a name server might be located.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsReplicaPointer Property Methods

The property method of the IADsReplicaPointer interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
ServerName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ServerName
(BSTR *retVal);

HRESULT put_ServerName
(BSTR bstrServerName);

 Name of the name server holding the replica.
ReplicaType

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ReplicaType
(LONG *retval);

HRESULT put_ReplicaType
(LONG lnType);

 Type of replica (master, secondary, or read-only.)
ReplicaNumber

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_ReplicaNumber
(LONG *retval);

HRESULT put_ReplicaNumber
(LONG lnReplicaNumber);

 Identification number of the replica.
Count

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Count
(LONG *retval);

HRESULT put_Count
(LONG lnCount);

 The number of existing replicas.
ReplicalAddressHints

[Visual Basic]
Access: Read/Write
Data Type: VARIANT

[C++]
HRESULT get_ReplicaAddressHints
(VARIANT *retval);

HRESULT put_ReplicaAddressHints
(VARIANT vReplicaAddressHints);

 A network address suggested as a likely reference to a node leading to the name server.

See Also

ADS_REPLICAPOINTER


IADsTimestamp

The IADsTimestamp interface provides methods for an ADSI client to access the Timestamp attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsTimestamp property methods | Description
get_WholeSeconds | Gets the number of whole seconds.
put_WholeSeconds | Puts the number of whole seconds.
get_EventID | Gets the event identifier.
put_EventID | Puts the event identifier.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsTimestamp Property Methods

The property method of the IADsTimestamp interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
WholeSeconds

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_WholeSeconds
(LONG *retVal);

HRESULT put_WholeSeconds
(LONG lnWholeSeconds);

 Number of seconds with zero value being 12:00 AM, January 1970, UTC.
EventID

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_EventID
(LONG *retval);

HRESULT put_EventID
(LONG lnEventID);

 An event identifier.

See Also

ADS_TIMESTAMP


IADsTypedName

The IADsTypedName interface provides methods for an ADSI client to access the Typed Name attribute in Novell NetWare® Directory Services (NDS).

You can call the property methods of this interface to obtain and modify the NDS attribute.

Methods in Vtable Order

IUnknown methods | Description
QueryInterface | Returns pointers to supported interfaces.
AddRef | Increments reference count.
Release | Decrements reference count.

IDispatch methods | Description
GetTypeInfoCount | Gets the number of type descriptions.
GetTypeInfo | Gets a description of object's programmable interface.
GetIDsOfNames | Maps name of method or property to DISPID.
Invoke | Calls one of the object's methods or gets and sets one of its properties.

IADsTypedName property methods | Description
get_ObjectName | Gets the object name.
put_ObjectName | Sets the object name.
get_Level | Gets the priority level of the object.
put_Level | Sets the priority level of the object.
get_Interval | Gets the frequency of references of the object.
put_Interval | Sets the frequency of references of object.

Requirements

Windows NT/2000: Requires Windows NT 4.0 or later.
Windows 95/98: Requires Windows 95 or later.
Header: Declared in iads.h.


IADsTypedName Property Methods

The property method of the IADsTypedName interface sets the property described in the following table. For more information, see Interface Property Methods.

Properties in Vtable Order

Property Description
ObjectName

[Visual Basic]
Access: Read/Write
Data Type: BSTR

[C++]
HRESULT get_ObjectName
(BSTR *retVal);

HRESULT put_ObjectName
(BSTR bstrObjectName);

 The name of an object.
Level

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Level
(LONG *retval);

HRESULT put_Level
(LONG lnLevel);

 The priority level associated with the object.
Interval

[Visual Basic]
Access: Read/Write
Data Type: LONG

[C++]
HRESULT get_Interval
(LONG *retval);

HRESULT put_Interval
(LONG lnInterval);

 The frequency of reference of the object.

See Also

ADS_TYPEDNAME


ADSI System Providers

ADSI system providers refer to those shipped with ADSI SDK. They are listed as follows:

  • LDAP. This is a namespace implementation that is compatible with Lightweight Directory Access Protocol (LDAP).
  • NDS. This is a namespace implementation compatible with Novell NetWare® Directory Service (NDS).
  • NWCOMPAT. This is a namespace implementation compatible with Novell NetWare® 3.x.
  • WinNT. This is a namespace implementation that is compatible with Windows NT®/Windows® 2000.

Different providers have different support of methods and property methods of the ADSI interfaces. This chapter presents such provider-specific information, including a summary description of provider support of ADSI interfaces.

In ADSI different providers are associated with different DLLs. The LDAP provider is associated with adsldp.dll, adsldpc.dll and adsmsext.dll. WinNT is with adsnt.dll, NDS with adsnds.dll, and NWCOMPAT with adsnw.dll. ROUTER corresponds to activeds.dll.


ADSI LDAP Provider

The ADSI LDAP provider implements a set of ADSI objects that support various ADSI interfaces. To access the LDAP provider, bind to any of the ADSI objects of LDAP, using the LDAP ADsPath.

Notice that you must have adsldp.dll, adsldpc.dll, adsmsext.dll, and Activeds.dll installed on the your client system in order to work with the provider.


LDAP ADsPath

An ADsPath string for the ADSI LDAP provider starts with either "LDAP:" or "LDAP://<>" where "<>" is an LDAP provider path.

For example, the following list of ADsPath names are valid:

LDAP:
LDAP://ldapsvr/O=Internet/DC=COM/DC=MSFT/DC=DEV/CN=TopHat
LDAP://ldapsvr/CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet
LDAP://MyDomain.microsoft.com/CN=TopH,DC=DEV,DC=MSFT,DC=COM,O=Internet
LDAP://CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet

The following table lists reserved characters for an ADsPath string in the LDAP provider.

Special Characters |  |
Double quote | " | Delimiter of ADsPath.
Backward slash | \ | Escape character, used to precede special characters to signify they should be used as literals.
Comma, Forward slash Semicolon | , / ; | Separates component from class specifier in last component.
Equal | = | Joins a naming class specifier with a component, for example, "OU=Finance".


Data Type Mapping between Active Directory and LDAP

The following topics are covered in this section:

  • Mapping LDAP Types to ADSTYPEs
  • Mapping between Friendly String Syntaxes and LDAP Types
  • Mapping between Syntax Object Identifiers and LDAP Types


Mapping LDAP Types to ADSTYPEs

The ADSTYPE enumeration defines data types used in ADSI. These data types can be mapped to appropriate LDAP counterparts. The following table presents such mapping.

ADSTYPE | LDAP Type
ADSTYPE_DN_STRING | LDAPTYPE_DN
ADSTYPE_CASE_EXACT_STRING | LDAPTYPE_CASEEXACTSTRING
ADSTYPE_CASE_IGNORE_STRING | LDAPTYPE_CASEIGNORESTRING LDAPTYPE_BITSTRING LDAPTYPE_DIRECTORYSTRING LDAPTYPE_COUNTRYSTRING LDAPTYPE_IA5STRING LDAPTYPE_OID LDAPTYPE_TELEPHONENUMBER LDAPTYPE_ATTRIBUTETYPEDESCRIPTION LDAPTYPE_OBJECTCLASSDESCRIPTION
ADSTYPE_CASE_IGNORE_STRING | LDAPTYPE_POSTALADDRESS LDAPTYPE_DELIVERYMETHOD LDAPTYPE_ENHANCEDGUIDE LDAPTYPE_FACSIMILETELEPHONENUMBER LDAPTYPE_GUIDE LDAPTYPE_NAMEANDOPTIONALUID LDAPTYPE_PRESENTATIONADDRESS LDAPTYPE_TELEXNUMBER LDAPTYPE_DSAQUALITYSYNTAX LDAPTYPE_DATAQUALITYSYNTAX LDAPTYPE_MAILPREFERENCE LDAPTYPE_OTHERMAILBOX LDAPTYPE_ACCESSPOINTDN LDAPTYPE_ORNAME
ADSTYPE_PRINTABLE_STRING | LDAPTYPE_PRINTABLESTRING
ADSTYPE_NUMERIC_STRING | LDAPTYPE_NUMERICSTRING
ADSTYPE_BOOLEAN | LDAPTYPE_BOOLEAN
ADSTYPE_INTEGER | LDAPTYPE_INTEGER
ADSTYPE_OCTET_STRING | LDAPTYPE_OCTETSTRING LDAPTYPE_CERTIFICATE LDAPTYPE_CERTIFICATELIST LDAPTYPE_CERTIFICATEPAIR LDAPTYPE_PASSWORD LDAPTYPE_TELETEXTERMINALIDENTIFIER LDAPTYPE_AUDIO LDAPTYPE_JPEG LDAPTYPE_FAX
ADSTYPE_NT_SECURITY_DESCRIPTOR | LDAPTYPE_SECURITY_DESCRIPTOR
ADSTYPE_UTC_TIME | LDAPTPYE_UTCTIME
ADSTYPE_UTC_TIME | LDAPTYPE_GENERALIZEDTIME
ADSTYPE_LARGE_INTEGER | LDAPTYPE_INTEGER8
E_ADS_CANT_CONVERT_DATATYPE | LDATYPE_UNKNOWN


Mapping between Friendly String Syntaxes and LDAP Types

The following table maps the LDAP types used in ADSI to user-friendly string syntax.

LDAP Types | Friendly String Syntax
LDAPTYPE_ACCESSPOINTDN | AccessPointDN
LDAPTYPE_ATTRIBUTETYPEDESCRIPTION | AttributeTypeDescription
LDAPTYPE_AUDIO | Audio
LDAPTYPE_OCTETSTRING | Binary
LDAPTYPE_BITSTRING | BitString
LDAPTYPE_BOOLEAN | Boolean
LDAPTYPE_CASEEXACTSTRING | CaseExactString
LDAPTYPE_CASEIGNORE_STRING | CaseIgnoreString
LDAPTYPE_CERTIFICATE | Certificate
LDAPTYPE_CERTIFICATELIST | CertificateList
LDAPTYPE_CERTIFICATEPAIR | CertificatePair
LDAPTYPE_COUNTRYSTRING | Country
LDAPTYPE_DATAQUALITYSYNTAX | DataQualitySyntax
LDAPTYPE_DELIVERYMETHOD | DeliveryMethod
LDAPTYPE_DIRECTORYSTRING | DirectoryString
LDAPTYPE_DN | DN
LDAPTYPE_DSAQUALITYSYNTAX | DSAQualitySyntax
LDAPTYPE_ENHANCED_GUIDE | EnhancedGuide
LDAPTYPE_FACSIMILETELEPHONENUMBER | FacsimileTelephoneNumber
LDAPTYPE_FAX | Fax
LDAPTYPE_GENERALIZEDTIME | GeneralizedTime
LDAPTYPE_GUIDE | Guide
LDAPTYPE_IA5STRING | IA5String
LDAPTYPE_INTEGER | INTEGER
LDAPTYPE_INTEGER8 | INTEGER8 (not in LDAP, NTDS)
LDAPTYPE_JPEG | JPEG
LDAPTYPE_MAILPREFERENCE | MailPreference
LDAPTYPE_NAMEANDOPTIONALUID | NameAndOptionalUID
LDAPTYPE_NUMERICSTRING | NumericString
LDAPTYPE_OBJECTCLASSDESCRIPTION | ObjectClassDescription
LDAPTYPE_SECURITY_DESCRIPTOR | ObjectSecurityDescriptor
LDAPTYPE_OCTETSTRING | OctetString (not in RFC)
LDAPTYPE_OID | OID
LDAPTYPE_ORNAME | ORName
LDAPTYPE_OTHERMAILBOX | OtherMailbox
LDAPTYPE_PASSWORD | Password
LDAPTYPE_POSTALADDRESS | PostalAddress
LDAPTYPE_PRESENTATIONADDRESS | PresentationAddress
LDAPTYPE_PRINTABLESTRING | PrintableString
LDAPTYPE_TELEPHONENUMBER | TelephoneNumber
LDAPTYPE_TELETEXTERMINALIDENTIFIER | TeletexTerminalIdentifier
LDAPTYPE_TELEXNUMBER | TelexNumber
LDAPTYPE_GENERALIZEDTIME | Time (only site server does this)
LDAPTYPE_UTCTIME | UTCTIME


Mapping between Syntax Object Identifiers and LDAP Types

The following table maps the LDAP types used in ADSI to the OID of the corresponding syntax.

LDAP Types | Syntax OID
LDAPTYPE_CASEIGNORESTRING | 1.2.840.113556.1.4.1221
LDAPTYPE_OCTETSTRIN | 1.2.840.113556.1.4.1222
LDAPTYPE_CASEEXACTSTRING | 1.2.840.113556.1.4.1362
LDAPTYPE_CASEIGNORESTRING | 1.2.840.113556.1.4.905
LDAPTYPE_INTEGER8 | 1.2.840.113556.1.4.906
LDAPTYPE_SECURITY_DESCRIPTOR | 1.2.840.113556.1.4.907
LDAPTYPE_CERTIFICATEPAIR | 1.3.6.1.4.1.1466.115.121.1.10
LDAPTYPE_COUNTRYSTRING | 1.3.6.1.4.1.1466.115.121.1.11
LDAPTYPE_DN | 1.3.6.1.4.1.1466.115.121.1.12
LDAPTYPE_DATAQUALITYSYNTAX | 1.3.6.1.4.1.1466.115.121.1.13
LDAPTYPE_DELIVERYMETHOD | 1.3.6.1.4.1.1466.115.121.1.14
LDAPTYPE_DIRECTORYSTRING | 1.3.6.1.4.1.1466.115.121.1.15
LDAPTYPE_DSAQUALITYSYNTAX | 1.3.6.1.4.1.1466.115.121.1.19
LDAPTYPE_ACCESSPOINTDN | 1.3.6.1.4.1.1466.115.121.1.2
LDAPTYPE_ENHANCEDGUIDE | 1.3.6.1.4.1.1466.115.121.1.21
LDAPTYPE_FACSIMILETELEPHONENUMBER | 1.3.6.1.4.1.1466.115.121.1.22
LDAPTYPE_FAX | 1.3.6.1.4.1.1466.115.121.1.23
LDAPTYPE_GENERALIZEDTIME | 1.3.6.1.4.1.1466.115.121.1.24
LDAPTYPE_GUIDE | 1.3.6.1.4.1.1466.115.121.1.25
LDAPTYPE_IA5STRING | 1.3.6.1.4.1.1466.115.121.1.26
LDAPTYPE_INTEGER | 1.3.6.1.4.1.1466.115.121.1.27
LDAPTYPE_JPEG | 1.3.6.1.4.1.1466.115.121.1.28
LDAPTYPE_ATTRIBUTETYPEDESCRIPTION | 1.3.6.1.4.1.1466.115.121.1.3
LDAPTYPE_MAILPREFERENCE | 1.3.6.1.4.1.1466.115.121.1.32
LDAPTYPE_NAMEANDOPTIONALUID | 1.3.6.1.4.1.1466.115.121.1.34
LDAPTYPE_NUMERICSTRING | 1.3.6.1.4.1.1466.115.121.1.36
LDAPTYPE_OBJECTCLASSDESCRIPTION | 1.3.6.1.4.1.1466.115.121.1.37
LDAPTYPE_OID | 1.3.6.1.4.1.1466.115.121.1.38
LDAPTYPE_OTHERMAILBOX | 1.3.6.1.4.1.1466.115.121.1.39
LDAPTYPE_AUDIO | 1.3.6.1.4.1.1466.115.121.1.4
LDAPTYPE_OCTETSTRING | 1.3.6.1.4.1.1466.115.121.1.40
LDAPTYPE_POSTALADDRESS | 1.3.6.1.4.1.1466.115.121.1.41
LDAPTYPE_PRESENTATIONADDRESS | 1.3.6.1.4.1.1466.115.121.1.43
LDAPTYPE_PRINTABLESTRING | 1.3.6.1.4.1.1466.115.121.1.44
LDAPTYPE_OCTETSTRING | 1.3.6.1.4.1.1466.115.121.1.5
LDAPTYPE_TELEPHONENUMBER | 1.3.6.1.4.1.1466.115.121.1.50
LDAPTYPE_TELETEXTERMINALIDENTIFIER | 1.3.6.1.4.1.1466.115.121.1.50
LDAPTYPE_TELEXNUMBER | 1.3.6.1.4.1.1466.115.121.1.52
LDAPTYPE_UTCTIME | 1.3.6.1.4.1.1466.115.121.1.53
LDAPTYPE_BITSTRING | 1.3.6.1.4.1.1466.115.121.1.6
LDAPTYPE_BOOLEAN | 1.3.6.1.4.1.1466.115.121.1.7
LDAPTYPE_CERTIFICATE | 1.3.6.1.4.1.1466.115.121.1.8
LDAPTYPE_CERTIFICATELIST | 1.3.6.1.4.1.1466.115.121.1.9


ADSI Objects of LDAP

The following is a list of commonly used ADSI object for the LDAP provider together with the ADSI interfaces they support.

ADSI Object | Description | Supported interfaces
GenObject | An ADSI object that represents the generic features of and provides common services to most other ADSI objects for the LDAP provider. | IADs IADsContainer IDirectoryObject IDirectorySearch IADsPropertyList IADsObjectOptions IADsDeleteOps
Namespace | An ADSI object that represents the LDAP namespace. | IADsContainer IADs IADsOpenDSObject
RootDSE | An ADSI object that represents the root DSE. | IADs IADsPropertyList
Property | An ADSI object that represents the attribute definition in the schema. | IADs IADsProperty
Class | An ADSI object that represents a class definition in the schema. | IADs IADsClass
Syntax | An ADSI object that represents the attribute syntax. | IADs IADsSyntax
Schema | An ADSI object that represents the schema container. | IADs IADsContainer
Pathname | An ADSI object that represents ADsPath. | IADsPathname
Group | An ADSI object that represents a group. | IADsGroup IADsExtension
GroupCollection | An ADSI object that represents a collection of groups. | IADsMembers
Locality | An ADSI object that represents geographical locales of a user, organization, etc. | IADsLocality IADsExtension
Organization | An ADSI object that represents an organization. | IADsO IADsExtension
OrganizationalUnit | An ADSI object that represents an organization unit. | IADsOU IADsExtension
PrintQueue | An ADSI object that represents a print queue. | IADsPrintQueue IADsPrintQueueOperations IADsExtension
User | An ADSI object that represents a user account. | IADsUser IADsExtension
UserCollection | An ADSI object that implements IADsMembers for maintaining and managing a collection of user accounts. | IADsMembers


LDAP Syntax Object

The LDAP provider uses the following mapping between the LDAP syntax and ADSI syntax.

LDAP Syntax | ADSI Syntax (Automation)
Boolean | VT_BOOL
Counter | VT_I4
ADsPath | VT_BSTR
EmailAddress | VT_BSTR
FaxNumber | VT_BSTR
Integer | VT_I4
Interval | VT_I4
List | VT_VARIANT (VT_BSTR | VT_ARRAY)
NetAddress | VT_BSTR
OctetString | VT_VARIANT
Path | VT_BSTR
PhoneNumber | VT_BSTR
PostalAddress | VT_BSTR
SmallInterval | VT_I4
String | VT_BSTR
Time | VT_DATE


LDAP User Object

The following topics are discussed for the LDAP-user object:

  • Mapping between IADsUser properties and Active Directory properties
  • User creation in Active Directory
  • Examples of user management in Active Directory

In addition, the user object in Active Directory contains many more properties than listed in the IADsUser Property Methods section. Because Active Directory Schema is extensible, other vendors may add additional attributes to a class, as well.


Mapping between IADsUser Properties and Active Directory Properties

When applicable, a property of ADSI user object is mapped to an appropriate Active Directory property. The ADSI user object properties are associated with the IADsUser property methods. The names of the Active Directory properties correspond to the ldapDisplayname attributes.

The following table summarizes the mapping between the IADsUser properties and their LDAP counterparts.

ADSI Property | Active Directory Property
AccountDisabled | userAccountControl Mask
AccountExpirationDate | accountExpires
BadLoginAddress | Not Supported
BadLoginCount | badPwdCount
Department | department
Description | description
Division | division
EmailAddress | mail
EmployeeID | employeeID
FaxNumber | facsimileTelephoneNumber
FirstName | givenName
FullName | displayName
GraceLoginsAllowed | Not Supported
GraceLoginsRemaining | Not Supported
HomeDirectory | homeDirectory
HomePage | wWWHomePage
IsAccountLocked | userAccountControl
Languages | language
LastFailedLogin | badPasswordTime
LastLogin | lastLogon
LastLogoff | lastLogoff
LastName | sn
LoginHours | logonHours
LoginScript | scriptPath
LoginWorkstations | userWorkstations
Manager | manager
MaxLogins | Not Supported
MaxStorage | maxStorage
NamePrefix | personalTitle
NameSuffix | generationQualifier
OfficeLocations | physicalDeliveryOfficeName
OtherName | middleName
PasswordExpirationDate | Set using Group Policy Editor*
PasswordLastChanged | pwdLastSet
PasswordMinimumLength | Set using Group Policy Editor*
PasswordRequired | userAccountControl mask
Picture | thumbnailPhoto
PostalAddresses | postalAddress
PostalCodes | postalCode
Profile | profilePath
RequireUniquePassword | Set using Group Policy Editor*
SeeAlso | seeAlso
TelephoneHome | homePhone
TelephoneMobile | mobile
TelephoneNumber | telephoneNumber
TelephonePager | pager
Title | title
* For Windows® 2000, group policy objects are not scriptable, you have to use the Microsoft® Management Console snap-in to set them. This will be fixed in the future release.


User Creation in Active Directory

In LDAP provider, you must at least specify the samAccountName attribute. You may default to the same value as the name you pass during a user creation.

In Windows® 2000, a user object can have several different names:

  • Common name (CN). This name will appear on most Windows 2000 tools. For example, John Smith.
  • SAM account name (samAccountName). Clients and servers that use previous versions of Windows NT® use this attribute.
  • User principal name (UPN). Windows 2000 clients may use a UPN to log on to a Windows 2000 domain controller. An example of a UPN is someone@microsoft.com. The UPN should be unique for a given Windows 2000 enterprise. A Windows 2000 enterprise (also known as a forest), may include more than Windows 2000 domains. You should check the global catalog (GC) to see if a UPN has been used.


User Creation with the ADSI LDAP Provider

With the LDAP provider, you can only create global user account. Local accounts reside in the SAM database and must be created using the WinNT provider.

When you create a user using the WinNT provider in Windows 2000, the new user will be placed in the user container of the domain. You may move this user to an organizational unit any time.

To create a user object, you must at least specify the common name (cn) and samAccountName attributes. When no additional attributes are specified, the user account will be created with the following default properties, in addition to the GUID and SID of the user object.

Property | Value
Full Name | Empty
CN (common name) | Must be specified explicitly.
First Name (givenName) | Empty
Last Name (sn) | Empty
User Principal Name (UPN) | Empty
SamAccountName | Must be specified explicitly.
Password | Empty
User Must Change Password | TRUE
User Cannot Change Password | FALSE
Password Never Expires | FALSE
Account Disabled | TRUE
Group | Domain User
Profile | Empty
Account Never Expires | TRUE

Example Code [Visual Basic]

The following Visual Basic code segment creates a user account with the default attributes as specified above.

Set ou = GetObject("LDAP://OU=PBS,DC=Microsoft,DC=COM")
Set usr = ou.Create("user", "cn=John Smith")
usr.Put "samAccountName", "jsmith"
usr.SetInfo

Example Code [C++]

The following C++ code snippet creates a user account with the default attributes as specified above. For brevity, error checking is omitted.

#include <activeds.h>

int main()
{
   HRESULT hr = CoInitialize(NULL);

   IADsContainer *pCont;
   IADsUser *pUser;

   LPWSTR adsPath = L"LDAP://serv1/CN=Users,dc=ArcadiaBay,dc=com";
   LPWSTR usrPass = L"adminSecrete";
   LPWSTR usrName = L"Administrator";

   hr = ADsOpenObject(adsPath,
                      usrName,
                      usrPass,
                      ADS_SECURE_AUTHENTICATION,
                      IID_IADsContainer,
                      (void**)&pCont);

   IDispatch *pDisp;
   hr = pCont->Create(L"user",L"cn=John Smith",&pDisp);
   pCont->Release();

   hr = pDisp->QueryInterface(IID_IADsUser,(void**)&pUser);
   pDisp->Release();

   VARIANT var;
   VariantInit(&var);
   V_BSTR(&var) = L"jsmith";
   V_VT(&var)=VT_BSTR;
   hr = pUser->Put(L"samAccountName",var);

   hr = pUser->SetInfo();

   VariantClear(&var);
   pUser->Release();

   CoUninitialize();

   return 0;
}


Examples of User Management in Active Directory

The following examples highlight the user management process in Active Directory through the LDAP provider.

  • User must change password at next logon
  • User cannot change password
  • Password never expires
  • Account disabled
  • Account expiration
  • Account lockout


User Must Change Password at Next Logon

To enable this option, set the pwdLastSet attribute to zero(0). To clear this flag, set the attribute to -1.

Example Code [Visual Basic]

Set usr = GetObject("LDAP://CN=John Smith, OU=Sales, DC=Acme, DC=Com")
usr.Put "pwdLastSet", CLng(0)
usr.SetInfo

Example Code [C++]

IADsUser *pUser;
HRESULT hr;

hr=ADsGetObject(L"LDAP://cn=John Smith,cn=users,dc=ArcadiaBay,dc=com",
                IID_IADsUser,
                (void**)&pUser);
VARIANT var;
VariantInit(&var);
V_I4(&var)=0;
V_VT(&var)=VT_I4;
hr = pUser->Put(L"pwdLastSet",var);
hr = pUser->SetInfo();

VariantClear(&var);
pUser->Release();


User Cannot Change Password

To enable this option, set the ADS_UF_PASSWD_CANT_CHANGE (0x0040) bit on the userAccountControl attribute. The flags are defined in the ADS_USER_FLAG enumeration.

The LDAP provider will not let you set both the "User Cannot Change Password" flag and the "User Must Change Password at Next Logon" flag at the same time. If you set both "Password Never Expires" and "User Must Change Password at Next Logon", the user will not be asked to change the password.

Example Code [Visual Basic]

Set usr = GetObject("LDAP:// CN=John Smith, OU=Sales, DC=ArcadiaBay, DC=Com" )
flag = usr.Get("UserAccountControl")
newFlag = flag Or ADS_UF_PASSWD_CANT_CHANGE
usr.Put "userAccountControl", newFlag
usr.SetInfo

Example Code [C++]

#include <activeds.h>

IADsUser *pUser;
VARIANT var;
VariantInit(&var);

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"LDAP://serv1/cn=John Smith,cn=Users, dc=ArcadiaBay, dc=com";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->Get(L"userAccountControl",&var);

V_I4(&var) |= ADS_UF_PASSWD_CANT_CHANGE;
hr = pUser->Put(L"userAccountControl",var);

hr = pUser->SetInfo();
VariantClear(&var);
hr = pUser->Release();


Password Never Expires

To enable this option, set the UF_DONT_EXPIRE_PASSWD(0x10000) bit on the userAccountControl attribute.

Example Code [Visual Basic]

Set usr = GetObject("LDAP:// CN=Jsmith, OU=Sales, DC=ArcadiaBay, DC=Com")
flag = usr.Get("UserAccountControl")
newFlag = flag Or ADS_UF_DONT_EXPIRE_PASSWD
usr.Put "userAccountControl", newFlag
usr.SetInfo

Example Code [C++]

#include <activeds.h>

IADsUser *pUser;
VARIANT var;
VariantInit(&var);

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"LDAP://serv1/cn=John Smith,cn=Users, dc=ArcadiaBay, dc=com";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->Get(L"userAccountControl",&var);

V_I4(&var) |= ADS_UF_DONT_EXPIRE_PASSWD;
hr = pUser->Put(L"userAccountControl",var);

hr = pUser->SetInfo();
VariantClear(&var);
hr = pUser->Release();


Account Disabled

You can disable a user account by setting the AccountDisabled property to True in the IADsUser interface. This is similar to the WinNT provider.

Example Code [Visual Basic]

Set usr = GetObject("LDAP:// CN=Jsmith, OU=Sales, DC=ArcadiaBay, DC=Com")
usr.AccountDisabled = TRUE 'disabled the account
usr.SetInfo

Example Code [C++]

IADsUser *pUser;

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"LDAP://serv1/cn=John Smith,cn=Users, dc=ArcadiaBay, dc=com";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->put_AccountDisabled(true);
hr = pUser->SetInfo();


Account Expiration

You can set the account expiration date by setting the AccountExpirationDate property of the IADsUser interface to a desired date value. To set the account expiration date to "never", set the AccountExpires attribute to –1.

Example Code [Visual Basic]

Set usr = GetObject("LDAP://CN=John smith, OU=Sales, DC=ArcadiaBay, DC=Com")
usr.AccountExpirationDate = "05/06/1998"
usr.SetInfo

// to enable the account forever,
usr.Put "AccountExpires",-1
usr.SetInfo

Example Code [C++]

void SetUserAccountExpirationDate(IADsUser *pUser, DATE date)
{
   if(!pUser) return;

   if(!date || date<0) {
      VARIANT var;
      VariantInit(&var);
      V_I4(&var)=-1;
      V_VT(&var)=VT_I4;
      hr = pUser->Put(L"accountExpires", var); // never expires.
      VariantClear(&var);
   }
   else {
      HRESULT hr = S_OK;
      hr = pUser->put_AccountExpirationDate(date); // expires on date
   }
   hr = pUser->SetInfo();
   hr = pUser->Release();
   return;
}


Account Lockout

Like the WinNT provider, you can only set the account lockout to FALSE. Only the system can set the account lockout to TRUE. You can also read the value of the account lockout.

Example Code [Visual Basic]

Set usr = GetObject("LDAP:// CN=John Smith, OU=Sales, DC=ArcadiaBay, DC=Com")
usr.IsAccountLocked = FALSE
usr.SetInfo

Example Code [C++]

IADsUser *pUser;

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"LDAP://serv1/cn=John Smith,cn=Users, dc=ArcadiaBay, dc=com";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->put_IsAccountLocked(false);
hr = pUser->SetInfo();

hr = pUser->Release();


ADSI WinNT Provider

The ADSI WinNT provider implements a set of ADSI objects to support various ADSI interfaces. To access the WinNT provider, bind to any of the ADSI objects of WinNT, using the WinNT AdsPath.

Notice that you need to have adsnt.dll and Activeds.dll installed on your client system in order to work with the provider.


WinNT ADsPath

The ADsPath string for the ADSI WinNT provider starts with either "WinNT:" or "WinNT://<DomainName>". For example,

"WinNT:"
"WinNT://MyDomain/MyServer/John"
"WinNT://MyDomain/MyServer/User"
"WinNT://MyDomain/JohnSmith"
"WinNT://MyDomain/User"
"WinNT://MyDomain/Administrator"
"WinNT://MyDomain/Group"
"WinNT://MyDomain/MyComputer/aPrinter/"

The WinNT ADsPath can also include the class name of the object you wish to bind. For example,

"WinNT://MyDomain/MyComputer/aPrinter,printQueue"
"WinNT://MyDomain/JohnSmith,user"

Using the class name improves the performance for binding.


ADSI Objects of WinNT

The ADSI WinNT provider implement the following ADSI objects to support features and services of various ADSI interfaces.

ADSI Object | Description | Supported Interfaces
Computer | An ADSI object that represents a computer. | IADs IADsComputer IADsComputerOperations IADsContainer IADsPropertyList
Domain | An ADSI object that represents a domain over the network. | IADs IADsDomain IADsContainer IADsPropertyList
FPNWResourcesCollection | An ADSI object that represents a collection of resources. | IADsCollection
FPNWSessionsCollection | An ADSI object that represents a collection of sessions. | IADsCollection
PrintJobsCollection | An ADSI object that represents a collection of print jobs. | IADsCollection
ResourcesCollection | An ADSI object that represents a collection of resources. | IADsCollection
SessionsCollection | An ADSI object that represents a collection of sessions. | IADsCollection
FPNWFileShare | An ADSI object that represents a file share. | IADs IADsFileShare IADsPropertyList
FPNWResource | An ADSI object that represents a resource. | IADs IADsResource IADsPropertyList
FPNWSession | An ADSI object that represents a session. | IADs IADsSession IADsPropertyList
FPNWFileService | An ADSI object that represents a file service. | IADs IADsFileService IADsFileServiceOperations IADsContainer
FileService | An ADSI object that represents a file service. | IADs IADsFileService IADsFileServiceOperations IADsContainer IADsPropertyList
FileShare | An ADSI object that represents a file share. | IADs IADsFileShare IADsPropertyList
Group | An ADSI object that represents a group. | IADs IADsGroup IADsPropertyList
GroupCollection | An ADSI object that represents a collection of groups. | IADs IADsMembers
PrintJob | An ADSI object that represents a print job. | IADs IADsPrintJob IADsPrintJobOperations IADsPropertyList
LocalgroupCollection | An ADSI object that represents a collection of local groups. | IADs IADsMembers
LocalGroup | An ADSI object that represents a local group. | IADs IADsGroup IADsPropertyList
Namespace | An ADSI object that represents the WinNT namespace. | IADs IADsContainer IADsOpenDSObject
PrintQueue | An ADSI object that represents a print queue. | IADs IADsPrintQueue IADsPrintQueueOperations IADsPropertyList
Resource | An ADSI object that represents a resource. | IADs IADsResource IADsPropertyList
Schema | An ADSI object that represents the schema container. | IADs IADsContainer
Property | An ADSI object that represents an attribute definition. | IADs IADsProperty
Class | An ADSI object that represents a class definition. | IADs IADsClass
Syntax | An ADSI object that represents the syntax of an attribute. | IADs IADsSyntax
Service | An ADSI object that represents a service. | IADs IADsService IADsServiceOperations IADsPropertyList
Session | An ADSI object that represents a session. | IADs IADsSession IADsPropertyList
User | An ADSI object that represents a user account. | IADs IADsUser IADsPropertyList
UserGroupCollection | An ADSI object that represents a collection of user groups. | IADsMembers


WinNT Schema

The WinNT schema defines the a set of ADSI objects and their class hierarchy for a Windows NT® 4.0 network. The ADSI objects of WinNT include User, Computer, Domain, Group, Resource, Session, Service, and others. The WinNT Object Class Hierarchy section below discusses the hierarchical relationships among these objects.

Each object implements a set of ADSI interfaces. However, of an implemented interface, not all properties and methods are supported. WinNT Schema's Mandatory and Optional Properties lists those properties supported by the WinNT object classes.


WinNT Object Class Hierarchy

The WinNT object class hierarchy starts from the Namespace object.

Object Class | Description
Namespace | Top-level object container.
Domain | The Windows NT domain.
User | User account.
Group | Group account for managing access rights.
UserGroupCollection | A set of user groups implementing IADsMember.
GroupCollection | A set of other groups implementing IADsMember.
Computer | Windows NT 4.0 server or workstation.
PrintJob | Print job in the print queue
PrintJobsCollection | A set of print jobs.
PrintQueue | Print queue on a printer spooler
Service | Application running as a service.
FileService | Services accessing file system
FileShare | File share point
Resource | A resource in the service
Session | An active file service connection
User | Local user account
Group | Local group
UserCollection | Collection of local users
GroupCollection | Collection of local groups.
Schema | WinNT Schema container.
Class | Schema class definition.
Property | Schema attribute definition.
Syntax | Syntax of a property.


WinNT Schema's Mandatory and Optional Properties

The following table lists the supported object properties (that is, the related ADSI interface property methods) in the WinNT provider. A property can be optional or mandatory.

Object Class | Supported Properties
Computer | Division Owner OperatingSystem OperatingSystemVersion Processor ProcessorCount | Optional Optional Optional Optional Optional Optional
Domain | MinPasswordLength MinPasswordAge MaxPasswordAge MaxBadPasswordsAllowed PasswordHistoryLength AutoUnlockInterval LockoutObservationInterval | Optional Optional Optional Optional Optional Optional Optional
FPNWFileService | HostComputer DisplayName Version ServiceType StartType Path ErrorControl LoadOrderGroup ServiceAccountName Dependencies Description MaxUserCount | Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional
FPNWFileShare | CurrentUserCount HostComputer Path MaxUserCount | Optional Optional Mandatory Mandatory
FPNWResource | User Path LockCount | Optional Optional Optional
FPNWSession | User Computer ConnectTime | Optional Optional Optional
FileService | HostComputer DisplayName Version ServiceType StartType Path ErrorControl LoadOrderGroup Description MaxUserCount ServiceAccountName Dependencies | Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional
FileShare | CurrentUserCount Description HostComputer Path MaxUserCount | Optional Optional Optional Mandatory Mandatory
Group | Description objectSid | Optional Optional
Namespace | IADs (all methods and properties.)
PrintJob | HostPrintQueue User TimeSubmitted TotalPages Size Description Priority StartTime UntilTime Notify TimeElapsed PagesPrinted Position Action ObjectGUID | Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional
PrintQueue | PrinterPath PrinterName Model Datatype PrintProcessor PrintDevices Description HostComputer Location StartTime UntilTime DefaultJobPriority JobCount Priority Attributes BannerPage ObjectGuid Action | mandatory mandatory mandatory mandatory mandatory optional optional Optional Optional Optional Optional Optional Optional Optional Optional Optional Optional
Resource | User Path LockCount | optional optional optional
Service | HostComputer LoadOrderGroup ServiceAccountName Dependencies StartType ServiceType DisplayName Path ErrorControl | Optional Optional Optional Optional Mandatory Mandatory Mandatory Mandatory Mandatory
Session | Computer ConnectTime IdleTime User | Optional Optional Optional Optional
User | AccountExpirationDate AutoUnlockInterval BadPasswordAttempts Description FullName HomeDirDrive HomeDirectory UserFlags LockoutObservationInterval LoginHours LastLogin LastLogoff LoginScript LoginWorkstations MinPasswordAge MinPasswordLength MaxBadPasswordsAllowed MaxLogins MaxPasswordAge MaxStorage objectSid Parameters PasswordAge PasswordExpirationDate PasswordExpired PasswordHistoryLength PrimaryGroupID Profile | Optional Optional Optional optional optional Optional Optional Optional Optional optional Optional Optional Optional optional Optional optional optional optional optional optional optional optional optional optional optional optional optional optional


WinNT User Object

The WinNT User object represents a user account in a Windows NT 4.0 domain. The object exhibits some special features. On one hand, it does not support all the property methods of the IADsUser interface. On the other hand, it supports some custom properties that can be accessed only through the IADs::Get or IADs::Put method.

The following topics are discussed in detail about the WinNT user object:

  • Unsupported IADsUser Property Methods
  • WinNT Custom User Properties
  • WinNT User Account Management Examples


Unsupported IADsUser Property Methods

The User class of the WinNT provider does not support the following property methods of the IADsUser interface.

Unsupported User Properties | Description
BadLoginAddress | See IADsUser Property Methods.
Department | See IADsUser Property Methods.
Division | See IADsUser Property Methods.
EmailAddress | See IADsUser Property Methods.
EmployeeID | See IADsUser Property Methods.
FaxNumber | See IADsUser Property Methods.
FirstName | See IADsUser Property Methods.
GraceLoginsAllowed | See IADsUser Property Methods.
GraceLoginsRemaining | See IADsUser Property Methods.
HomePage | See IADsUser Property Methods.
Languages | See IADsUser Property Methods.
LastFailedLogin | See IADsUser Property Methods.
LastName | See IADsUser Property Methods.
Manager | See IADsUser Property Methods.
NamePrefix | See IADsUser Property Methods.
NameSuffix | See IADsUser Property Methods.
OfficeLocations | See IADsUser Property Methods.
OtherName | See IADsUser Property Methods.
PasswordLastChanged | See IADsUser Property Methods.
Picture | See IADsUser Property Methods.
PostalAddresses | See IADsUser Property Methods.
PostalCodes | See IADsUser Property Methods.
RequireUniquePassword | See IADsUser Property Methods.
SeeAlso | See IADsUser Property Methods.
TelephoneHome | See IADsUser Property Methods.
TelephoneMobile | See IADsUser Property Methods.
TelephoneNumber | See IADsUser Property Methods.
TelephonePager | See IADsUser Property Methods.
Title | See IADsUser Property Methods.


WinNT Custom User Properties

The WinNT provider makes available the following custom properties for the User class. They may be accessed through the IADs::Get and IADs::Put methods.

Property | Type | Comments
HomeDirDrive | String | Home Directory Drive of the user.
ObjectSID | Octet String | Object SID of the user.
Parameters | String | Parameters of the user. Points to a Unicode string that is set aside for use by applications. This string can be a null string, or it can have any number of characters before the terminating null character. Microsoft® products use this member to store user configuration information.
PasswordAge | Time | Time duration of the password in use.
PasswordExpired | Time | Time when the password was expired.
PrimaryGroupID | Integer | User's primary group ID, for example, domain user group ID.
UserFlags | Integer | User Flag defined in ADS_USER_FLAG.


WinNT User Account Management Examples

The following examples show how to work with some of the user properties using the WinNT provider:

  • User must change password at next logon
  • User cannot change password
  • Password never expires
  • Account disabled
  • Account expiration
  • Account lockout
  • Primary Group ID
  • Object SID


User Must Change Password at Next Logon

To enable this option, set the PasswordExpired attribute to one(1). Setting this attribute to 0 allows the user to log on without changing the password.

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/jsmith,user")
usr.Put "PasswordExpired", CLng(1)   ' user must change password
usr.SetInfo

Example Code [C++]

IADsUser *pUser;
HRESULT hr;

hr=ADsGetObject(L"WinNT://ArcadiaBay/jsmith,user",
                IID_IADsUser,
                (void**)&pUser);
VARIANT var;
VariantInit(&var);
V_I4(&var)=1;
V_VT(&var)=VT_I4;
hr = pUser->Put(L"PasswordExpired",var); // user must change password
hr = pUser->SetInfo();

VariantClear(&var);
pUser->Release();


User Cannot Change Password

To enable this option, set the ADS_UF_PASSWD_CANT_CHANGE (0x0040) bit on the UserFlags attribute. The flags are defined in the ADS_USER_FLAG enumeration.

The WinNT provider will not let you set both the "User Cannot Change Password" flag and the "User Must Change Password at Next Logon" flag at the same time. If you set both "Password Never Expires" and "User Must Change Password at Next Logon", the user will not be asked to change the password.

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/jsmith,user")
flag = usr.Get("UserFlags")
newFlag = flag Or ADS_UF_PASSWD_CANT_CHANGE
usr.Put "userFlags", newFlag
usr.SetInfo

Example Code [C++]

#include <activeds.h>

IADsUser *pUser;
VARIANT var;
VariantInit(&var);

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"WinNT://ArcadiaBay/jsmith,user";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->Get(L"userFlags",&var);

V_I4(&var) |= ADS_UF_PASSWD_CANT_CHANGE;
hr = pUser->Put(L"userFlags",var);

hr = pUser->SetInfo();
VariantClear(&var);
hr = pUser->Release();


Password Never Expires

To enable this option, set the UF_DONT_EXPIRE_PASSWD(0x10000) bit on the UserFlags attribute.

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
flag = usr.Get("UserFlags")
newFlag = flag Or ADS_UF_DONT_EXPIRE_PASSWD
usr.Put "userFlags", newFlag
usr.SetInfo

Example Code [C++]

#include <activeds.h>

IADsUser *pUser;
VARIANT var;
VariantInit(&var);

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"WinNT://ArcadiaBay/JSmith";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->Get(L"UserFlags",&var);

V_I4(&var) |= ADS_UF_DONT_EXPIRE_PASSWD;
hr = pUser->Put(L"UserFlags",var);

hr = pUser->SetInfo();
VariantClear(&var);
hr = pUser->Release();


Account Disabled

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
usr.AccountDisabled = TRUE 'disabled the account
usr.SetInfo

Example Code [C++]

IADsUser *pUser;

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"WinNT://ArcadiaBay/JSmith";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->put_AccountDisabled(true);
hr = pUser->SetInfo();


Account Expiration

Account Expiration Date can be set using the AccountExpiration method in the IADsUser interface.

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
usr.AccountExpirationDate = "05/06/1998"
usr.SetInfo

To set the account expiration date to "never", use "January 1, 1970" as the parameter.

You can set the account expiration date by setting the AccountExpirationDate property of the IADsUser interface to a desired date value. To set the account expiration date to "never", set this property to "Janurary 1, 1970".

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
usr.AccountExpirationDate = "05/06/1998"
usr.SetInfo

// to enable the account forever,
usr.AccountExpirationDate = "01/01/1970"
usr.SetInfo

Example Code [C++]

void SetUserAccountExpirationDate(IADsUser *pUser, DATE date)
{
   if(!pUser) return;

   HRESULT hr = S_OK;
   hr = pUser->put_AccountExpirationDate(date); // expires on date

   hr = pUser->SetInfo();
   hr = pUser->Release();
   return;
}


Account Lockout

As with the LDAP provider, you can only set the account lockout to FALSE. Only the system can set the account lockout to TRUE. You can also read the value of the account lockout.

Example Code [Visual Basic]

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
usr.IsAccountLocked = FALSE
usr.SetInfo

Example Code [C++]

IADsUser *pUser;

HRESULT hr = S_OK;
LPWSTR adsPath;
adsPath=L"WinNT://ArcadiaBay/JSmith";
hr = ADsGetObject(adsPath,IID_IADsUser,(void**)&pUser);

hr = pUser->put_IsAccountLocked(false);
hr = pUser->SetInfo();

hr = pUser->Release();


Primary Group ID

An integer value represents the primary group ID for this user.

Set usr = GetObject("WinNT://ArcadiaBay/JSmith")
Debug.Print usr.Get("PrimaryGroupID")


Object SID

The user's SID can be retrieved using the objectSID attribute. The objectSID is returned as a VARIANT array of the string.

sid = usr.Get("objectSID")
For i = LBound(sid) To UBound(sid)
    Debug.Print sid(i)
Next


ADSI NDS Provider

The ADSI NDS provider implements a set of ADSI objects to provide support to Novell NetWare Directory Service. To access the NDS provider, bind to any of the ADSI objects of NDS, using the NDS AdsPath.

Note Adsnds.dll and Activeds.dll must be installed on the client system in order for that client to be able to have access to the provider.


NDS ADsPath

An ADsPath string for the ADSI NDS provider starts with either "NDS:" or "NDS://<>", where <> is an NDS provider path. For example:

"NDS:"
"NDS://ndsServer/O=Internet/DC=COM/DC=MSFT/DC=DEV/CN=TopHat"
"NDS://ndsServer/CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet"
"NDS://MyDom.microsoft.com/CN=TopH,DC=DEV,DC=MSFT,DC=COM,O=Internet"


ADSI Objects of NDS

The ADSI NDS provider implements the following objects that support various ADSI interfaces.

ADSI Objects | Description | Supported Interfaces
Acl | An ADSI object that represents an access control list (ACL). | IADsAcl
BackLink | An ADSI object that represents the Back Link attribute in NDS. | IADsBackLink
CaseIgnoreList | An ADSI object that represents a list of case insensitive strings. | IADsCaseIgnoreList
Class | An ADSI object that represents a class definition. | IADs IADsClass
Email | An ADSI object that represents an email account. | IADsEmail
FaxNumber | An ADSI object that represents a fax number. | IADsFaxNumber
GenObject | An ADSI object that represents a generic object providing basic object features and common services to most of the ADSI objects in the NDS provider. | IADs IADsContainer IDirectoryObject IDirectorySearch IADsPropertyList
Group | An ADSI object that represents a group. | IADs IADsGroup IDirectoryObject IDirectorySearch IADsPropertyList
GroupCollection | An ADSI object that represents a collection of groups. | IADs IADsMembers
Hold | An ADSI object that represents the Hold attribute in NDS. | IADsHold
Locality | An ADSI object that represents the geographical locale of a user account, an organization, etc. | IADsLocality IADsContainer IDirectoryObject IDirectorySearch IADsPropertyList
Namespace | An ADSI object that represents the namespace. | IADs IADsContainer IADsOpenDSObject
NetAddress | An ADSI object that represents the NetAddress attribute in NDS. | IADsNetAddress
OctetList | An ADSI object that represents a list of octet string. | IADsOctetList
Organization | An ADSI object that represents an organization. | IADsO IADsContainer IDirectoryObject IDirectorySearch IADsPropertyList
OrganizationalUnit | An ADSI object that represents an organization unit. | IADsOU IADsContainer IDirectoryObject IDirectorySearch IADsPropertyList
Path | An ADSI object that represents the Path attribute in NDS. | IADsPath
PostalAddress | An ADSI object that represents a postal address. | IADsPostalAddress
PrintQueue | An ADSI object that represents a print queue. | IADsPrintQueue IADsPrintQueueOperations IADsPropertyList
Property | An ADSI object that represents an attribute definition. | IADs IADsProperty
ReplicaPointer | An ADSI object that represents the ReplicaPointer attribute in NDS. | IADsReplicaPointer
Schema | An ADSI object that represents the schema container. | IADs IADsContainer
Syntax | An ADSI object that represents the syntax of an attribute. | IADs IADsSyntax
Timestamp | An ADSI object that represents the Timestamp attribute in NDS. | IADsTimestamp
Tree | An ADSI object that represents a NDS directory tree. | IADs IADsContainer
TypedName | An ADSI object that represents the TypedName attribute in NDS. | IADsTypedName
User | An ADSI object that represents a user account. | IADs IADsUser IDirectoryObject IDirectorySearch IADsPropertyList
UserCollection | An ADSI object that represents a collection of user accounts. | IADs IADsMembers


ADSI NWCOMPAT Provider

The ADSI NWCOMPAT provider implements a set of ADSI objects to support directory services provided by the Novell NetWare 3.x. To access the NWCOMPAT provider, bind to any of the ADSI objects of NWCOMPAT, using the NWCOMPAT ADsPath.

Note Adsnds.dll and Activeds.dll must be installed on the client system in order for that client to be able to have access to the provider.


NWCOMPAT ADsPath

An ADsPath string for the ADSI NWCOMPAT provider starts with either "NWCOMPAT:" or "NWCOMPAT://<>", where <> is an NWCOMPAT provider path. For example,

NWCOMPAT:
NWCOMPAT://ndsServer/O=Internet/DC=COM/DC=MSFT/DC=DEV/CN=TopHat
NWCOMPAT://ndsServer/CN=TopHat,DC=DEV,DC=MSFT,DC=COM,O=Internet
NWCOMPAT://Domain.Arcadia.com/CN=TopH,DC=Arcadia,DC=COM,O=Internet


ADSI Objects of NWCOMPAT

The ADSI NWCOMPAT provider implements the following objects that support various ADSI interfaces.

ADSI Object | Description | Supported Interfaces
Computer | An ADSI object represents a computer on the network. | IADs IADsComputer IADsComputerOperations IADsContainer IADsPropertyList
FileService | An ADSI object represents a file service. | IADs IADsFileService IADsFileServiceOperations IADsContainer IADsPropertyList
FileShare | An ADSI object represents a file share on the network. | IADs IADsFileShare IADsPropertyList
Group | An ADSI object represents a group account. | IADs IADsGroup IADsPropertyList
GroupCollection | An ADSI object represents a collection of groups. | IADs IADsMembers
PrintJob | An ADSI object represents a print job. | IADs IADsPrintJob IADsPrintJobOperations IADsPropertyList
JobCollection | An ADSI object represents a collection of print jobs. | IADs IADsCollection
Namespace | An ADSI objects represents the namespace of the directory. | IADs IADsContainer
PrintQueue | An ADSI object represents a print queue. | IADsPrintQueue IADsPrintQueueOperations IADsPropertyList IADs
Property | An ADSI object represents an attribute definition of the schema. | IADs IADsProperty
Class | An ADSI object represents a class definition of the schema. | IADs IADsClass
Syntax | An ADSI object represents the syntax of an attribute. | IADs IADsSyntax
Schema | An ADSI object represents the schema container of the provider. | IADs IADsContainer
User | An ADSI object represents a user account. | IADs IADsUser IADsPropertyList
UserCollection | An ADSI object represents a collection of users. | IADs IADsMembers


ADSI ROUTER

The ADSI router sits between a client and the ADSI providers. It implements a core set of objects that offer basic features and services common to all the providers. Thus, any feature supported in the router is also supported in any of the provider unless the provider chooses otherwise. The ADSI router module corresponds to Activeds.dll.


ADSI Objects Implemented in the Router Layer

The following table presents a brief description of the ADSI objects implemented in the router.

ADSI Object | Description | Supported Interfaces
AccessControlEntry | An ADSI object that represents an access control entry (ACE). | IADsAccessControlEntry
AccessControlList | An ADSI object that represents an access control list (ACL). | IADsAccessControlList
LargeInteger | An ADSI object that represents a large integer. | IADsLargeInteger
ADsNamespaces | An ADSI object that represents the container of various namespaces. | IADs IADsContainer IADsNamespaces
PropertyEntry | An ADSI object that represents a property entry. | IADsPropertyEntry
SecurityDescriptor | An ADSI object that represents a security descriptor. | IADsSecurityDescriptor
PropertyValue | An ADSI object that represents a property value. | IADsPropertyValue IADsValue
PropertyValue2 | An ADSI object that represents a property value | IADsPropertyValue2


Provider Support of ADSI Interfaces

The following table presents an "at-glance" description of which interfaces are supported by the providers that are shipped with ADSI 2.5. An entry marked with "Yes" indicates that at least one ADSI object of the specified provider supports the associated interface. "No" indicates that no object of the provider supports the interface in this release. In the future, currently unsupported interfaces may become supported by the system-supplied providers.

For more information about ADSI provider-specific implementation details, see

  • ADSI LDAP Provider
  • ADSI WinNT Provider
  • ADSI NDS Provider
  • ADSI NWCOMPAT Provider
  • ADSI ROUTER

For information on which property or method is supported for each interface, follow the links by clicking the appropriate interface name in the first column of the table.

Interface Name | LDAP | WinNT | NDS | NWCOMPAT
IADs | Yes | Yes | Yes | Yes
IADsAccessControlEntry | Yes | No | Yes | No
IADsAccessControlList | Yes | No | Yes | No
IADsAcl | No | No | Yes | No
IADsBackLink | No | No | Yes | No
IADsCaseIgnoreList | No | No | Yes | No
IADsClass | Yes | Yes | Yes | Yes
IADsCollection | No | Yes | No | Yes
IADsComputer | No | Yes | No | Yes
IADsComputerOperations | No | Yes | No | Yes
IADsContainer | Yes | Yes | Yes | Yes
IADsDeleteOps | Yes | No | No | No
IADsDomain | No | Yes | No | No
IADsEmail | No | No | Yes | No
IADsExtension | Yes | Yes | No | Yes
IADsFaxNumber | No | No | Yes | No
IADsFileService | No | Yes | No | Yes
IADsFileServiceOperations | No | Yes | No | Yes
IADsFileShare | No | Yes | No | Yes
IADsGroup | Yes | Yes | Yes | Yes
IADsHold | No | No | Yes | No
IADsLargeInteger | Yes | No | No | No
IADsLocality | Yes | No | Yes | No
IADsMembers | Yes | Yes | Yes | Yes
IADsNamespaces | Yes | Yes | Yes | Yes
IADsNetAddress | No | No | Yes | No
IADsO | Yes | No | Yes | No
IADsOU | Yes | No | Yes | No
IADsObjectOptions | Yes | No | No | No
IADsOctetList | No | No | Yes | No
IADsOpenDSObject | Yes | Yes | Yes | No
IADsPath | No | No | Yes | No
IADsPathname | Yes | Yes | Yes | Yes
IADsPostalAddress | No | No | Yes | No
IADsPrintJob | No | Yes | No | Yes
IADsPrintJobOperations | No | Yes | No | Yes
IADsPrintQueue | Yes | Yes | Yes | Yes
IADsPrintQueueOperations | Yes | Yes | Yes | Yes
IADsProperty | Yes | Yes | Yes | Yes
IADsPropertyEntry | Yes | Yes | Yes | Yes
IADsPropertyList | Yes | Yes | Yes | Yes
IADsPropertyValue | Yes | Yes | Yes | Yes
IADsPropertyValue2 | Yes | Yes | Yes | Yes
IADsReplicaPointer | No | No | Yes | No
IADsResource | No | Yes | No | No
IADsSecurityDescriptor | Yes | No | Yes | No
IADsService | No | Yes | No | No
IADsServiceOperations | No | Yes | No | No
IADsSession | No | Yes | No | No
IADsSyntax | Yes | Yes | Yes | Yes
IADsTimestamp | No | No | Yes | No
IADsTypedName | No | No | Yes | No
IADsUser | Yes | Yes | Yes | Yes
IDirectoryObject | Yes | No | Yes | No
IDirectorySearch | Yes | No | Yes | No

Provider Support for IADsUser

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
AccountDisabled | Supported | Supported
AccountExpirationDate | Supported | Supported
BadLoginAddress | Not supported | Not supported
BadLoginCount | Supported | Supported
Department | Supported | Not supported
Description | Supported | Supported
Division | Supported | Not supported
EmailAddress | Supported | Not supported
EmployeeID | Supported | Not supported
FaxNumber | Supported | Not supported
FirstName | Supported | Not supported
FullName | Supported | Supported
GraceLoginsAllowed | Not Supported | Not supported
GraceLoginsRemaining | Not Supported | Not supported
HomeDirectory | Supported | Supported
HomePage | Supported | Not supported
IsAccountLocked | Supported | Supported
Languages | Not Supported | Not supported
LastFailedLogin | Supported | Not supported
LastLogin | Supported | Supported
LastLogoff | Supported | Supported
LastName | Supported | Not supported
LoginHours | Supported | Supported
LoginScript | Supported | Supported
LoginWorkstations | Supported | Supported
Manager | Supported | Not supported
MaxLogins | Not supported | Not supported
MaxStorage | Supported | Supported
NamePrefix | Supported | Not supported
NameSuffix | Supported | Not supported
OfficeLocations | Supported | Not supported
OtherName | Supported | Not supported
PasswordExpirationDate | Supported | Supported
PasswordLastChanged | Supported | Not supported
PasswordMinimumLength | Supported | Supported
PasswordRequired | Supported | Supported
Picture | Supported | Not supported
PostalAddresses | Supported | Not supported
PostalCodes | Supported | Not supported
Profile | Supported | Supported
RequireUniquePassword | Supported | Not supported
SeeAlso | Supported | Not supported
TelephoneHome | Supported | Not supported
TelephoneMobile | Supported | Not supported
TelephoneNumber | Supported | Not supported
TelephonePager | Supported | Not supported
Title | Supported | Not supported

Provider Support for IADsComputer

[This is preliminary documentation and subject to change.]

Properties | LDAP | WinNT
ComputerID | Interface not supported | Not supported
Department | Interface not supported | Not supported
Description | Interface not supported | Not supported
Division | Interface not supported | Supported
Location | Interface not supported | Not supported
MemorySize | Interface not supported | Not supported
Model | Interface not supported | Not supported
NetAddresses | Interface not supported | Not supported
OperatingSystem | Interface not supported | Supported
OperatingSystemVersion | Interface not supported | Supported
Owner | Interface not supported | Supported
PrimaryUser | Interface not supported | Not supported
Processor | Interface not supported | Supported
ProcessorCount | Interface not supported | Supported
Role | Interface not supported | Not supported
Site | Interface not supported | Not supported
StorageCapacity | Interface not supported | Not supported

Provider Support for IADsComputerOperations

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
Shutdown | Interface not supported | Not implemented
Status | Interface not supported | Not implemented

Provider Support for IADsDomain

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
IsWorkgroup | Interface not supported | Not implemented
MinPasswordLength | Interface not supported | Supported
MinPasswordAge | Interface not supported | Supported
MaxpasswordAge | Interface not supported | Supported
MaxBadPasswordsAllowed | Interface not supported | Supported
PasswordHistoryLength | Interface not supported | Supported
PasswordAttributes | Interface not supported | Not supported
AutoUnlockInterval | Interface not supported | Supported
LockoutObservationInterval | Interface not supported | Supported

Provider Support for IADsFileService

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
Description | Interface not supported | Supported
MaxUserCount | Interface not supported | Supported

Provider Support for IADsGroup

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
Description | Supported | Supported

Provider Support for IADsClass

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
PrimaryInterface | Supported | Supported
CLSID | Supported | Supported
OID | Supported | Supported
Abstract | Supported | Supported
Auxiliary | Supported | Supported
MandatoryProperties | Supported | Supported
OptionalProperties | Supported | Supported
NamingProperties | Supported | Not implemented
DerivedFrom | Supported | Not supported
AuxDerivedFrom | Supported | Not supported
PossibleSuperiors | Supported | Supported
Containment | Supported for read | Supported
Container | Supported for read | Supported
HelpFileName | Supported | Supported
HelpFileContext | Supported | Supported
Method | LDAP | WinNT
Qualifiers | Not implemented | Not implmented

Provider Support for IADsProperty

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
OID | Supported | Supported
Syntax | Supported | Supported
MaxRange | Supported | Supported
MinRange | Supported | Supported
Multivalued | Supported | Supported

Provider Support for IADsSyntax

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
OleAutoDataType | Supported | Supported

Provider Support for IADsContainer

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
Count | Not implemented | Not implemented
Hints | Supported | Not implemented
Filter | Supported | Supported

Provider Support for IADsNamespaces

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
defaultContainer | Supported | Supported

Provider Support for IADsAccessControlEntry

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
AccessMask | Supported | Not supported
AccessType | Supported | Not supported
AccessFlags | Supported | Not supported
Flags | Supported | Not supported
ObjectType | Supported | Not supported
InheritedObjectType | Supported | Not supported
Trustee | Supported | Not supported

Provider Support for IADsAccessControlList

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
AceCount | Supported | Not supported
AceRevision | Supported | Not supported
Method | LDAP | WinNT
AddAce | Supported | Not supported
CopyAccessList | Supported | Not supported
RemoveAce | Supported | Not supported
get__NewEnum | Supported | Not supported

Provider Support for IADsSecurityDescriptor

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
Control | Supported | Not supported
DaclDefaulted | Supported | Not supported
DiscretionaryAcl | Supported | Not supported
Group | Supported | Not supported
GroupDefaulted | Supported | Not supported
Owner | Supported | Not supported
OwnerDefaulted | Supported | Not supported
SaclDefaulted | Supported | Not supported
SystemAcl | Supported | Not supported
Method | LDAP | WinNT
CopySecurityDescriptor | Supported | Not supported

Provider Support for IADsObjectOptions

[This is preliminary documentation and subject to change.]

Method | LDAP | WinNT
GetObjectOption | Supported | Interface not supported
SetObjectOption | Supported | Interface not supported

Provider Support for IADsCollection

[This is preliminary documentation and subject to change.]

Method | LDAP | WinNT
Add | Interface not supported | Not supported
get__NewEnum | Interface not supported | Supported
GetObject | Interface not supported | Supported
Remove | Interface not supported | Supported

Provider Support for IADsMembers

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT | NDS | NWCOMPAT
Count | Supported for GroupCollection, but not for UserCollection | Not supported | Not supported | Not supported
Filter | Supported | Supported | Supported | Supported
Method | LDAP | WinNT | NDS | NWCOMPAT
get__NewEnum | Supported | Supported | Supported | Supported

Provider Support for IADsPathname

[This is preliminary documentation and subject to change.]

Property | LDAP |  | WinNT
EscapedMode | Supported |  | Supported
Method | LDAP | WinNT
Set | Supported |  | Supported
SetDisplayType | Supported |  | Supported
Retrieve | Supported |  | Supported
GetNumElements | Supported |  | Supported
GetElement | Supported |  | Supported
GetEscapedElement | Supported |  | Not supported
RemoveLeafElement | Supported |  | Supported
CopyPath | Supported |  | Supported

Provider Support for IADsPrintQueue

[This is preliminary documentation and subject to change.]

Property | LDAP | WinNT
PrinterPath | Supported | Not supported
Model | Supported | Supported.
Datatype | Not supported | Supported
PrintProcessor | Not supported | Supported
Description | Supported | Supported
Location | Supported | Supported
StartTime | Supported | Supported
UntilTime | Supported | Supported
DefaultJobPriority | Not supported | Supported
Priority | Supported | Supported
BannerPage | Supported | Supported
PrintDevices | Supported | Supported
NetAddresses | Not supported | Not supported

Provider Support for IADsPrintQueueOperations

[This is preliminary documentation and subject to change.]

Property | LDAP |  | WinNT
Status | Supported |  | Supported
Method | LDAP | WinNT
Pause | Supported |  | Supported.
PrintJobs | Supported |  | Supported
Purge | Supported |  | Supported
Resume | Supported |  | Supported


ADSI Error Codes

All ADSI error codes are returned as COM HRESULT values. They can be grouped into the following three types:

  • Generic COM error codes
  • Generic ADSI error codes
  • Win32 error codes for ADSI

In addition, certain interfaces provide additional error information, known as ADSI extended error messages, which can be obtained using the ADsGetLastError function.

Finally, the C++ example code given in this section shows how to work ADSI error messages.


Generic COM Error Codes

Generic COM error codes expresses the operation status as produced by all COM modules. The following table contains some examples of such error codes on any Win32 platform.

Error Code | Hex Value | Description
E_UNEXPECTED | 0x8000FFFF | Catastrophic failure.
E_NOTIMPL | 0x80004001 | Not implemented.
E_NOINTERFACE | 0x80004002 | Interface not supported.
E_POINTER | 0x80004003 | Invalid pointer.
E_ABORT | 0x80004004 | Operation aborted.
E_FAIL | 0x80004005 | Unspecified error.


Generic ADSI Error Codes

This type of error codes may be returned by any of the ADSI system providers to represent ADSI-specific error messages. They have the facility code 5 with severity bit set either TRUE or FALSE. Setting the severity bit to TRUE results in error values of the form 0x80005xxx and error messages of the form E_ADS_*. When the severity bit is set to FALSE, the resulting error values are of the form 0x00005xxx and the error messages are of the S_ADS_* form.

Generic ADSI error codes are defined in adserr.h.

Value | Code | Description | Corrective Action
0x80005000L | E_ADS_BAD_PATHNAME | An invalid ADSI pathname was passed. | Verify that the object exists on the directory server and check for typos of the path.
0x80005001L | E_ADS_INVALID_DOMAIN_OBJECT | An unknown ADSI domain object was requested. | Verify the path of the domain object.
0x80005002L | E_ADS_INVALID_USER_OBJECT | An unknown ADSI user object was requested. | Verify the existence of the user object, check for typos of the path name and the user's access rights.
0x80005003L | E_ADS_INVALID_COMPUTER_OBJECT | An unknown ADSI computer object was requested. | Verify the existence of the computer object, check for typos of the path name and the computer's access rights.
0x80005004L | E_ADS_UNKNOWN_OBJECT | An unknown ADSI object was requested. | Verify the name of and the access rights to the object.
0x80005005L | E_ADS_PROPERTY_NOT_SET | The specified ADSI property was not set. |
0x80005006L | E_ADS_PROPERTY_NOT_SUPPORTED | The specified ADSI property is not supported. | Verify the correct property is being set.
0x80005007L | E_ADS_PROPERTY_INVALID | The specified ADSI property is invalid | Verify the parameters passed to the method call.
0x80005008L | E_ADS_BAD_PARAMETER | One or more input parameters are invalid. |
0x80005009L | E_ADS_OBJECT_UNBOUND | The specified ADSI object is not bound to a remote resource. | Make sure to call GetInfo on a newly created object after SetInfo has been called.
0x8000500AL | E_ADS_PROPERTY_NOT_MODIFIED | The specified ADSI object has not been modified. |
0x8000500BL | E_ADS_PROPERTY_MODIFIED | The specified ADSI object has been modified.
0x8000500CL | E_ADS_CANT_CONVERT_DATATYPE | The data type cannot be converted to/from a native DS data type. | Make sure that the correct data type is used and/or that there is sufficient schema information available to perform data type conversion.
0x8000500DL | E_ADS_PROPERTY_NOT_FOUND | The property cannot be found in the cache. | Make sure that GetInfo has been called (implicitly or explicitly). If the problem persists, the property has not been set on the server.
0x8000500EL | E_ADS_OBJECT_EXISTS | The ADSI object exists. | Use a different name to create the object.
0x8000500FL | E_ADS_SCHEMA_VIOLATION | The attempted action violates the directory service schema rules. |
0x80005010L | E_ADS_COLUMN_NOT_SET | The specified column in the ADSI was not set.
0x00005011L | S_ADS_ERRORSOCCURRED | During a query, one or more errors occurred. | Verify that the search preference can be legally set and, if so, is properly set
0x00005012L | S_ADS_NOMORE_ROWS | The search operation has reached the last row. | Move on to the rest of the program.
0x00005013L | S_ADS_NOMORE_COLUMNS | The search operation has reached the last column for the current row. | Move on to next row.
0x80005014L | E_ADS_INVALID_FILTER | The specified search filter is invalid. | Make sure to use the correct format of the filter that is acceptable to the directory server.


Win32 Error Codes for ADSI

Standard Win32 error codes are also used to return ADSI error messages. In particular, the ADSI LDAP provider maps all the LDAP error codes to Win32 error codes. The HRESULT values of these error codes are of the 0x8007XXXX format, where the last four hexadecimal digits, XXXX, corresponds to the DWORD values of the appropriate Win32 error code. For example, the ADSI error value 0x80072020 gives the Win32 error value of 0x2020 in hexadecimal or 8224 in decimal.

Most of the Win32 error codes for ADSI are defined in Winerror.h or Lmerr.h. The error values are listed as decimal values in these files. To convert the HRESULT value of an ADSI error code (produced by your application) to the corresponding the Win32 errors DWORD value (defined in the header files above), do the following:

  1. Convert the HRESULT value, hrErr, to a hexadecimal number from the decimal, if starting with a decimal value as you may get from a Visual Basic® application.
  2. Drop the 0x8007 part from hrErr to produce the remainder, dwErr,
  3. Convert dwErr back to a decimal number,
  4. Look dwErr up in Winerror.h.
  5. If not found in Winerror.h, subtract 2100 from dwErr and look up the result in Lmerr.h.

ADSI 2.0 maps the LDAP error codes to a set of Win32 error codes that is different from that used in ADSI 2.5. The differences are listed in the two tables below:

  • Win32 Error Codes for ADSI 2.5
  • Win32 Error Codes for ADSI 2.0


Win32 Error Codes for ADSI 2.5

The following table lists the LDAP error messages and their corresponding Win32 error codes for ADSI.

ADSI Error Value | LDAP message | Win32 message | Description
0L | LDAP_SUCCESS | NO_ERROR | Operation succeeded.
0x80070005L | LDAP_INSUFFICIENT_RIGHTS | ERROR_ACCESS_DENIED | The user has insufficient access right.
0x80070008L | LDAP_NO_MEMORY | ERROR_NOT_ENOUGH_MEMORY | The system is out of memory.
0x8007001fL | LDAP_OTHER | ERROR_GEN_FAILURE | Unknown error occurred.
0x800700eaL | LDAP_PARTIAL_RESULTS | ERROR_MORE_DATA | Partial results and referrals received.
0x800700eaL | LDAP_MORE_RESULTS_TO_RETURN | ERROR_MORE_DATA | More results are to be returned.
0x800704c7L | LDAP_USER_CANCELLED | ERROR_CANCELLED | The user has cancelled the operation.
0x800704c9L | LDAP_CONNECT_ERROR | ERROR_CONNECTION_REFUSED | Cannot establish the connection.
0x8007052eL | LDAP_INVALID_CREDENTIALS | ERROR_LOGON_FAILURE | The supplied credential is invalid.
0x800705b4L | LDAP_TIMEOUT | ERROR_TIMEOUT | The search was timed out.
0x80071392L | LDAP_ALREADY_EXISTS | ERROR_OBJECT_ALREADY_EXISTS | The object already exists.
0x8007200aL | LDAP_NO_SUCH_ATTRIBUTE | ERROR_DS_NO_ATTRIBUTE_OR_VALUE | Requested attribute does not exist.
0x8007200bL | LDAP_INVALID_SYNTAX | ERROR_DS_INVALID_ATTRIBUTE_SYNTAX | The syntax is invalid.
0x8007200cL | LDAP_UNDEFINED_TYPE | ERROR_DS_ATTRIBUTE_TYPE_UNDEFINED | Type is not defined.
0x8007200dL | LDAP_ATTRIBUTE_OR_VALUE_EXISTS | ERROR_DS_ATTRIBUTE_OR_VALUE_EXISTS | The attribute exists or the value has been assigned.
0x8007200eL | LDAP_BUSY | ERROR_DS_BUSY | The server is busy.
0x8007200fL | LDAP_UNAVAILABLE | ERROR_DS_UNAVAILABLE | The server is not available.
0x80072014L | LDAP_OBJECT_CLASS_VIOLATION | ERROR_DS_OBJ_CLASS_VIOLATION | There was an object class violation.
0x80072015L | LDAP_NOT_ALLOWED_ON_NONLEAF | ERROR_DS_CANT_ON_NON_LEAF | Operation is not allowed on a non leaf object.
0x80072016L | LDAP_NOT_ALLOWED_ON_RDN | ERROR_DS_CANT_ON_RDN | Operation is not allowed on RDN.
0x80072017L | LDAP_NO_OBJECT_CLASS_MODS | ERROR_DS_CANT_MOD_OBJ_CLASS | Cannot modify object class.
0x80072020L | LDAP_OPERATIONS_ERROR | ERROR_DS_OPERATIONS_ERROR | Operations error occurred.
0x80072021L | LDAP_PROTOCOL_ERROR | ERROR_DS_PROTOCOL_ERROR | Protocol error occurred.
0x80072022L | LDAP_TIMELIMIT_EXCEEDED | ERROR_DS_TIMELIMIT_EXCEEDED | Time limit has exceeded
0x80072023L | LDAP_SIZELIMIT_EXCEEDED | ERROR_DS_SIZELIMIT_EXCEEDED | Size limit has exceeded
0x80072024L | LDAP_ADMIN_LIMIT_EXCEEDED | ERROR_DS_ADMIN_LIMIT_EXCEEDED | Administration limit on the server has exceeded.
0x80072025L | LDAP_COMPARE_FALSE | ERROR_DS_COMPARE_FALSE | Compare yielded FALSE.
0x80072026L | LDAP_COMPARE_TRUE | ERROR_DS_COMPARE_TRUE | Compare yielded TRUE.
0x80072027L | LDAP_AUTH_METHOD_NOT_SUPPORTED | ERROR_DS_AUTH_METHOD_NOT_SUPPORTED | The authentication method is not supported.
0x80072028L | LDAP_STRONG_AUTH_REQUIRED | ERROR_DS_STRONG_AUTH_REQUIRED | Strong authentication is required.
0x80072029L | LDAP_INAPPROPRIATE_AUTH | ERROR_DS_INAPPROPRIATE_AUTH | Authentication is inappropriate.
0x8007202aL | LDAP_AUTH_UNKNOWN | ERROR_DS_AUTH_UNKNOWN | Unknown authentication error occurred.
0x8007202bL | LDAP_REFERRAL | ERROR_DS_REFERRAL | Referral
0x8007202cL | LDAP_UNAVAILABLE_CRIT_EXTENSION | ERROR_DS_UNAVAILABLE_CRIT_EXTENSION | Critical extension is unavailable.
0x8007202dL | LDAP_CONFIDENTIALITY_REQUIRED | ERROR_DS_CONFIDENTIALITY_REQUIRED | Confidentiality is required.
0x8007202eL | LDAP_INAPPROPRIATE_MATCHING | ERROR_DS_INAPPROPRIATE_MATCHING | There was an inappropriate matching.
0x8007202fL | LDAP_CONSTRAINT_VIOLATION | ERROR_DS_CONSTRAINT_VIOLATION | There was a constrain violation.
0x80072030L | LDAP_NO_SUCH_OBJECT | ERROR_DS_NO_SUCH_OBJECT | Object does not exist.
0x80072031L | LDAP_ALIAS_PROBLEM | ERROR_DS_ALIAS_PROBLEM | The alias is invalid.
0x80072032L | LDAP_INVALID_DN_SYNTAX | ERROR_DS_INVALID_DN_SYNTAX | The distinguished name has an invalid syntax.
0x80072033L | LDAP_IS_LEAF | ERROR_DS_IS_LEAF | The object is a leaf.
0x80072034L | LDAP_ALIAS_DEREF_PROBLEM | ERROR_DS_ALIAS_DEREF_PROBLEM | Can not dereference the alias.
0x80072035L | LDAP_UNWILLING_TO_PERFORM | ERROR_DS_UNWILLING_TO_PERFORM | The server is unwilling to perform.
0x80072036L | LDAP_LOOP_DETECT | ERROR_DS_LOOP_DETECT | Loop was detected.
0x80072037L | LDAP_NAMING_VIOLATION | ERROR_DS_NAMING_VIOLATION | There was a naming violation.
0x80072038L | LDAP_RESULTS_TOO_LARGE | ERROR_DS_OBJECT_RESULTS_TOO_LARGE | Results returned are too large.
0x80072039L | LDAP_AFFECTS_MULTIPLE_DSAS | ERROR_DS_AFFECTS_MULTIPLE_DSAS | Multiple directory service agents are affected.
0x8007203aL | LDAP_SERVER_DOWN | ERROR_DS_SERVER_DOWN | Cannot contact the LDAP server.
0x8007203bL | LDAP_LOCAL_ERROR | ERROR_DS_LOCAL_ERROR | Local error occurred.
0x8007203cL | LDAP_ENCODING_ERROR | ERROR_DS_ENCODING_ERROR | Encoding error occurred.
0x8007203dL | LDAP_DECODING_ERROR | ERROR_DS_DECODING_ERROR | Decoding error occurred.
0x8007203eL | LDAP_FILTER_ERROR | ERROR_DS_FILTER_UNKNOWN | The search filter is bad.
0x8007203fL | LDAP_PARAM_ERROR | ERROR_DS_PARAM_ERROR | A bad parameter was passed to a routine.
0x80072040L | LDAP_NOT_SUPPORTED | ERROR_DS_NOT_SUPPORTED | The feature is not supported.
0x80072041L | LDAP_NO_RESULTS_RETURNED | ERROR_DS_NO_RESULTS_RETURNED | Results are not returned.
0x80072042L | LDAP_CONTROL_NOT_FOUND | ERROR_DS_CONTROL_NOT_FOUND | The control was not found.
0x80072043L | LDAP_CLIENT_LOOP | ERROR_DS_CLIENT_LOOP | Client loop was detected.
0x80072044L | LDAP_REFERRAL_LIMIT_EXCEEDED | ERROR_DS_REFERRAL_LIMIT_EXCEEDED | The referral limit has exceeded.


Win32 Error Codes for ADSI 2.0

The following table lists the LDAP error messages and their corresponding Win32 error codes for ADSI 2.0.

ADSI Error Value | LDAP message | Win32 message | Description
0 | LDAP_SUCCESS | NO_ERROR | Operation succeeded.
0x80070002 | LDAP_NO_SUCH_OBJECT | ERROR_FILE_NOT_FOUND | Object does not exist.
0x80070005 | LDAP_AUTH_METHOD_NOT_SUPPORTED | ERROR_ACCESS_DENIED | The authentication method is not supported.
0x80070005 | LDAP_STRONG_AUTH_REQUIRED | ERROR_ACCESS_DENIED | Strong authentication is required.
0x80070005 | LDAP_INAPPROPRIATE_AUTH | ERROR_ACCESS_DENIED | Authentication is inappropriate.
0x80070005 | LDAP_INSUFFICIENT_RIGHTS | ERROR_ACCESS_DENIED | The user has insufficient access right.
0x80070005 | LDAP_AUTH_UNKNOWN | ERROR_ACCESS_DENIED | Unknown authentication error occurred.
0x80070008 | LDAP_NO_MEMORY | ERROR_NOT_ENOUGH_MEMORY | The system is out of memory.
0x8007001F | LDAP_OTHER | ERROR_GEN_FAILURE | Unknown error occurred.
0x8007001F | LDAP_LOCAL_ERROR | ERROR_GEN_FAILURE | Local error occurred.
0x80070037 | LDAP_UNAVAILABLE | ERROR_DEV_NOT_EXIST | The server is not available.
0x8007003A | LDAP_SERVER_DOWN | ERROR_BAD_NET_RESP | Cannot contact the LDAP server.
0x8007003B | LDAP_ENCODING_ERROR | ERROR_UNEXP_NET_ERR | Encoding error occurred.
0x8007003B | LDAP_DECODING_ERROR | ERROR_UNEXP_NET_ERR | Decoding error occurred.
0x80070044 | LDAP_ADMIN_LIMIT_EXCEEDED | ERROR_TOO_MANY_NAMES | Administration limit on the server has exceeded.
0x80070056 | LDAP_INVALID_CREDENTIALS | ERROR_INVALID_PASSWORD | The supplied credential is invalid.
0x80070057 | LDAP_INVALID_DN_SYNTAX | ERROR_INVALID_PARAMETER | The distinguished name has an invalid syntax.
0x80070057 | LDAP_NAMING_VIOLATION | ERROR_INVALID_PARAMETER | There was a naming violation.
0x80070057 | LDAP_OBJECT_CLASS_VIOLATION | ERROR_INVALID_PARAMETER | There was an object class violation.
0x80070057 | LDAP_FILTER_ERROR | ERROR_INVALID_PARAMETER | The search filter is bad.
0x80070057 | LDAP_PARAM_ERROR | ERROR_INVALID_PARAMETER | A bad parameter was passed to a routine.
0X8007006E | LDAP_OPERATIONS_ERROR | ERROR_OPEN_FAILED | Operations error occurred.
0x8007007A | LDAP_RESULTS_TOO_LARGE | ERROR_INSUFFICIENT_BUFFER | Results returned are too large.
0x8007007B | LDAP_INVALID_SYNTAX | ERROR_INVALID_NAME | The syntax is invalid.
0x8007007C | LDAP_PROTOCOL_ERROR | ERROR_INVALID_LEVEL | Protocol error occurred.
0x800700B7 | LDAP_ALREADY_EXISTS | ERROR_ALREADY_EXISTS | The object already exists.
0x800700EA | LDAP_PARTIAL_RESULTS | ERROR_MORE_DATA | Partial results and referrals received.
0x800700EA | LDAP_BUSY | ERROR_BUSY | The server is busy.
0x800703EB | LDAP_UNWILLING_TO_PERFORM | ERROR_CAN_NOT_COMPLETE | The server is unwilling to perform.
0x8007041D | LDAP_TIMEOUT | ERROR_SERVICE_REQUEST_TIMEOUT | The search was timed out.
0x800704B8 | LDAP_COMPARE_FALSE | ERROR_EXTENDED_ERROR | Compare yielded FALSE.
0x800704B8 | LDAP_COMPARE_TRUE | ERROR_EXTENDED_ERROR | Compare yielded TRUE.
0x800704B8 | LDAP_REFERRAL | ERROR_EXTENDED_ERROR | Referral
0x800704B8 | LDAP_UNAVAILABLE_CRIT_EXTENSION | ERROR_EXTENDED_ERROR | Critical extension is unavailable.
0x800704B8 | LDAP_NO_SUCH_ATTRIBUTE | ERROR_EXTENDED_ERROR | Requested attribute does not exist.
0x800704B8 | LDAP_UNDEFINED_TYPE | ERROR_EXTENDED_ERROR | Type is not defined.
0x800704B8 | LDAP_INAPPROPRIATE_MATCHING | ERROR_EXTENDED_ERROR | There was an inappropriate matching.
0x800704B8 | LDAP_CONSTRAINT_VIOLATION | ERROR_EXTENDED_ERROR | There was a constrain violation.
0x800704B8 | LDAP_ATTRIBUTE_OR_VALUE_EXISTS | ERROR_EXTENDED_ERROR | The attribute exists or the value has been assigned.
0x800704B8 | LDAP_ALIAS_PROBLEM | ERROR_EXTENDED_ERROR | The alias is invalid.
0x800704B8 | LDAP_IS_LEAF | ERROR_EXTENDED_ERROR | The object is a leaf.
0x800704B8 | LDAP_ALIAS_DEREF_PROBLEM | ERROR_EXTENDED_ERROR | Can not dereference the alias.
0x800704B8 | LDAP_LOOP_DETECT | ERROR_EXTENDED_ERROR | Loop was detected.
0x800704B8 | LDAP_NOT_ALLOWED_ON_NONLEAF | ERROR_EXTENDED_ERROR | Operation is not allowed on a non leaf object.
0x800704B8 | LDAP_NOT_ALLOWED_ON_RDN | ERROR_EXTENDED_ERROR | Operation is not allowed on RDN.
0x800704B8 | LDAP_NO_OBJECT_CLASS_MODS | ERROR_EXTENDED_ERROR | Cannot modify object class.
0x800704B8 | LDAP_AFFECTS_MULTIPLE_DSAS | ERROR_EXTENDED_ERROR | Multiple directory service agents are affected.
0x800704C7 | LDAP_USER_CANCELLED | ERROR_CANCELLED | The user has cancelled the operation.
0x80070718 | LDAP_TIMELIMIT_EXCEEDED | ERROR_NOT_ENOUGH_QUOTA | Time limit has exceeded
0x80070718 | LDAP_SIZELIMIT_EXCEEDED | ERROR_NOT_ENOUGH_QUOTA | Size limit has exceeded

In ADSI 2.0, several LDAP error messages are mapped to a Win32 error code as ERROR_EXTENDED_ERROR. Call ADsGetLastError to retrieve the error string returned by the server. For more information, see ADSI Extended Error Messages below.


ADSI Extended Error Messages

Apart from the HRESULT values, several ADSI system providers (mostly LDAP, but also NDS and NWCOMPAT) return additional error information for all operations performed by the following interfaces:

  • IADs
  • IADsContainer
  • IDirectoryObject
  • IDirectorySearch

A part of such extended error information is the string sent by the server as a part of the message result.

You call ADsGetLastError to retrieve such extended error messages. The first parameter of this function, lpError, is a DWORD value. For an Active Directory™ server, ADSI will attempt to map an LDAP error message to an appropriate Win32 error code and assigns the Win32 error code value to lpError. Failing to resolve the mapping, ADSI assigns ERROR_INVALID_DATA to lpError, as it does for any other directory server. In all the cases, ADSI will faithfully relay the string of the error description from the server to the client through lpErrorBuf, the second parameter of the ADsGetLastError function.


Code Example for Working with ADSI Error Messages

CString GetErrorMessage( HRESULT hr )
{
    BOOL bRet;
    CString s;
    LPTSTR lpBuffer=NULL;

    if ( SUCCEEDED(hr) )
    {
        return _T("Success");
    }

    if ( hr & 0x00005000) // standard ADSI Errors
    {
        s = GetADSIError(hr);
    }
    else if ( HRESULT_FACILITY(hr)==FACILITY_WIN32 )
    {

        /////////////////////////////////////////////
        // Retrieve the Win32 Error message
        /////////////////////////////////////////////

        bRet = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
                             FORMAT_MESSAGE_FROM_SYSTEM,
                             NULL,  hr,
                             MAKELANGID(LANG_NEUTRAL,
                             SUBLANG_SYS_DEFAULT),
                             (LPTSTR) &lpBuffer, 0, NULL);

        if ( !bRet )
        {
            s.Format(_T("Error %ld"), hr );
        }

        if ( lpBuffer )
        {
            s = lpBuffer;
            LocalFree( lpBuffer );
        }
    }
    else // Non Win32 Error
    {
        s.Format("%X", hr );
        return s;
    }

    //////////////////////////////////////////////////////////////////
    //
    // Extended error message may be occured.
    //
    // IADs, IADsContainer, IDirectoryObject or IDirectorySearch may
    // return this extended error message
    //
    /////////////////////////////////////////////////////////////////
    WCHAR szBuffer[MAX_PATH];
    WCHAR szName[MAX_PATH];
    DWORD dwError;

    hr = ADsGetLastError( &dwError, szBuffer, (sizeof(szBuffer)/sizeof(WCHAR))-1,
                          szName, (sizeof(szName)/sizeof(WCHAR))-1 );

    if ( SUCCEEDED(hr) && dwError != ERROR_INVALID_DATA  &&
                                     wcslen(szBuffer))
    {
        USES_CONVERSION;
        s += _T("  -- Extended Error --- ");
        s += OLE2T(szName);
        s += _T(" : ");
        s += OLE2T( szBuffer );
    }

    return s;
}

typedef struct tagADSERRMSG
{
    HRESULT    hr;
    LPCTSTR    pszError;
}ADSERRMSG;

#define ADDADSERROR(x)   x, _T(#x)

ADSERRMSG adsErr[] =
{
    ADDADSERROR(E_ADS_BAD_PATHNAME),
    ADDADSERROR(E_ADS_INVALID_DOMAIN_OBJECT),
    ADDADSERROR(E_ADS_INVALID_USER_OBJECT),
    ADDADSERROR(E_ADS_INVALID_COMPUTER_OBJECT),
    ADDADSERROR(E_ADS_UNKNOWN_OBJECT),
    ADDADSERROR(E_ADS_PROPERTY_NOT_SET),
    ADDADSERROR(E_ADS_PROPERTY_NOT_SUPPORTED),
    ADDADSERROR(E_ADS_PROPERTY_INVALID),
    ADDADSERROR(E_ADS_BAD_PARAMETER),
    ADDADSERROR(E_ADS_OBJECT_UNBOUND),
    ADDADSERROR(E_ADS_PROPERTY_NOT_MODIFIED),
    ADDADSERROR(E_ADS_PROPERTY_MODIFIED),
    ADDADSERROR(E_ADS_CANT_CONVERT_DATATYPE),
    ADDADSERROR(E_ADS_PROPERTY_NOT_FOUND),
    ADDADSERROR(E_ADS_OBJECT_EXISTS),
    ADDADSERROR(E_ADS_SCHEMA_VIOLATION),
    ADDADSERROR(E_ADS_COLUMN_NOT_SET),
    ADDADSERROR(E_ADS_INVALID_FILTER),
    ADDADSERROR(0),
};

/////////////////////////////////////////////
//
// Error message specific to ADSI
//
////////////////////////////////////////////
CString GetADSIError( HRESULT hr )
{
    CString s;

    if ( hr & 0x00005000 )
    {
        int idx=0;
        while (adsErr[idx].hr != 0 )
        {
            if ( adsErr[idx].hr == hr )
            {
                return adsErr[idx].pszError;
            }
            idx++;
        }
    }

    return _T("");

}


A

Automation
Automation technology is a way to manipulate objects within a scripting environment. Methods within an Automation environment are typically accessed through the IDispatch interface.
access-control entry (ACE)
An access-control entry defines object permissions for a user or group. It contains a security identifier (SID), and information about the type of access the ACE grants or denies.
access-control list (ACL)
An access-control list is an ordered collection of ACEs.
action policy
An action is a discrete step or set of steps performed as the result of evaluating a condition or set of conditions in a policy. Actions affect the interaction between the subject and the object. For example, when a user (the subject) connects to a RADIUS server (the object), the conditions in the policy are evaluated and a profile is selected (the action). Note that in many cases the action is simple enough that it is implied by the policy.
Active Directory Service Interfaces (ADSI)
Active Directory™ Service Interfaces is a set of specifications for COM objects and interfaces. Administrators and developers can use ADSI objects to perform common administrative operations, such as adding new users or managing a print queue.
attribute
An attribute is a single property of an object. An object is described by the values of its attributes; for example, a car can be described by its make, model, color, and so on These are the attributes of the car. The term "attribute" is often used interchangeably with "property". An attribute is the definition. A property is an instance of the attribute on an object (see Attribute Instance).
attribute instance
An attribute instance is an occurrence of an attribute that is defined in the schema. This term is used to distinguish between the definition of an attribute and a discrete occurrence of the attribute. For example, storing a user object for "James Smith" with the common-name attribute set to "James Smith" creates an instance of common-name.
authoritative restore
An authoritative restore is a restore of a backup copy of a Window NT 2000 domain controller in which the objects in the restored directory are treated as authoritative. This means that the restored objects are propagated using replication, replacing existing copies of those objects.


B

backup domain controller
In a Windows NT® server 4.0 or earlier domain, the backup domain controller is the server host computer that receives a copy of the domain's directory database, with all account and security policy information for the domain. The copy is synchronized periodically and automatically with the master copy on the primary domain controller (PDC). BDCs also authenticate user logons and can be promoted to function as PDCs as needed. Multiple BDCs can exist on a domain.

In a Windows® 2000 domain, backup domain controllers are not required; all domain controllers are peers, and all can perform maintenance on the directory. Windows NT 4.0 and 3.51 Backup domain controllers can participate in a Windows NT 2000 domain when it is running in mixed mode.


C

canonical name
The canonical name for an object is the distinguished name, rendered in a more friendly way, namely, root first, "/" delimited, and without the LDAP attribute tags (CN=, DC=).

For example:

CN=Foo,OU=MyOU,DC=Microsoft,DC=Com

is rendered as

Microsoft.Com/MyOU/Foo

in canonical form.

class
In ADSI, a class is a formal description of a discrete, identifiable type of object that can be stored in a directory service. For example, user, print-queue, and group are all classes. These classes, which act as templates, are stored within a directory service's schema.
class instance
A class instance is a specific occurrence of a class defined in a directory service schema. For example, user objects with the attributes "James Smith" or "Martha Dale" would represent instances of the user class.
client (directory client)
A directory client is a server, workstation, or application that accesses a directory service using the LDAP protocol to query the directory for objects of interest.
collection
A collection is a group of objects that can be indexed, either numerically like an array or by name. ADSI containers support interface methods for manipulating collections.
common name (CN)
A common name is used in distinguished names (DNs) to indicate a property of the object itself. For example, /O=Internet/DC=COM/DC=Microsoft/CN=Users/CN=James Smith contains the common names "users" and "James Smith".
Component Object Model (COM)
The Component Object Model is a programming model that defines the way in which software components communicate with and provide services to one another, regardless of where the components reside. ADSI defines a set of COM interfaces (and related APIs) for working with directory service objects. For more information, see the COM SDK.
component
A component is an object that conforms to the Component Object Model (COM). In ADSI, a component is a top-level object that can be instantiated, and then used to access dependent objects.
condition policy
A condition is a single rule within a policy. A policy can have many conditions. The conditions are evaluated to determine what action to take.
constraint
Class and attribute definitions can include constraints. For object class definitions, the constraints are the content rules and structure rules, as described above. For attribute definitions, the constraints are the value range (for integers) and the minimum and maximum length (for strings).
container
A container is any ADSI object that supports one or more of the methods of the IADsContainer interface. A container has attributes and is part of Active Directory namespace. An ADSI container can hold one or more other directory service objects or containers.


containment

Microsoft Active Directory SDK
containment
The object instances in the directory are arranged in a hierarchy, or tree. The topmost object in the directory is an object called the root. In a tree, an object can be a leaf or a container. An object is a container if it is the parent of other objects; an object is a leaf if it is not the parent of other objects. The distinction between leaf and container objects is determined by the structure rules.
content rules
Content rules define the possible attributes of the class instances stored in the directory service. In Active Directory, the content rules are completely expressed by the must-have and may-have attributes of the schema definitions for each class.
cross-reference
Active Directory stores knowledge information about the naming contexts in cross-reference objects. A cross-reference provides information to the directory system agent (DSA) to use to generate referrals.

Cross-references can refer to naming contexts that are part of the local forest, or that are external. An example of an external naming context is a separate Lightweight Directory Access Protocol (LDAP) directory used in an enterprise that also has Windows 2000 installed. By creating a cross-reference for the separate LDAP directory, Active Directory DSA can generate referrals to the external directory when client queries reference it.


D

data model
Active Directory data model is derived from the X.500 data model. The directory holds objects that represent various things described by attributes. The universe of objects that can be stored in the directory is defined in the schema. For each object class, the schema defines what attributes an instance of the class must have, what additional attributes it may have, and what object class can be a parent of the current object class.
decision point
A decision point is a discrete location in the network where policy selection occurs. Policy selection identifies the set of relevant candidate policies for evaluation from the multitude of policies that might be available. The conditions of the policies selected are evaluated at the decision point to determine the specific policies that apply. These policies are enforced at enforcement points. Decision points and enforcement points are often co-located, that is, policies are selected and enforced at the same place.
delegation
Delegation is one of the most important security features of Active Directory. Delegation allows a higher administrative authority to grant specific administrative rights for containers and subtrees to individuals and groups. This eliminates the need for domain administrators with sweeping authority over large segments of the user population. Access-control entries (ACEs) can grant specific administrative rights on the objects in a container to a user or group. Rights are granted for specific operations on specific object classes using ACEs in the container's access-control list (ACL).
directory
A directory is an information source used to store information about objects. A telephone directory stores information about telephone subscribers. In a file system, the directory stores information about files. In a distributed computing system or a public computer network like the Internet, there are many objects, such as printers, fax servers, applications, databases, and other users.
directory service (DS)
A directory service differs from a directory in that it is both the directory information source, and the services making the information available and usable to the users.

A directory service is one of the most important components of an extended computer system. Users and administrators frequently do not know the exact name of the objects they are interested in. They may know one of more attributes of the objects and can query the directory to get a list of objects that match the attributes. For example, "find all duplex printers in Building 26." A directory service allows a user to find any object given one of its attributes.

A directory service can:

  • Enforce security defined by administrators to keep information safe from intruders
  • Distribute a directory across many computers in a network
  • Replicate a directory to make it available to more users and resistant to failure
  • Partition a directory into multiple stores to allow the storage of a very large numbers of objects

    A directory service is both a management tool and an end user tool. As the number of objects in a network grows, the directory service becomes essential. The directory service is the hub around which a large distributed system turns.

directory system agent (DSA)
A directory system agent is a process that manages the directory's physical storage.
discretionary access-control list (DACL)
A discretionary access-control list is a list that is controlled by the owner of an object and that specifies the access that particular users or groups can have to the object.
distinguished name (DN)
A distinguished name is an identifier for an object that is unique across the entire directory. For example, /O=Internet/DC=COM/DC=Microsoft/CN=Users/CN=James Smith refers to the "James Smith" user object in the Microsoft.com domain.
domain
A domain is a single security boundary of a Windows NT computer network. Active Directory is made up of one or more domains. On a standalone workstation, the domain is the computer itself. A domain can span more than one physical location. Every domain has its own security policies and security relationships with other domains. When multiple domains are connected by trust relationships and share a common schema, configuration, and global catalog, you have a tree. Multiple trees can be connected together into a forest.
domain component (DC)
A domain component is used in distinguished names (DNs) to indicate an identifier for a part of an object's network domain. For example, /O=Internet/DC=COM/DC=Microsoft/ CN=Users/CN=John Smith contains the Domain Components "COM" and "Microsoft".
domain controller
A domain controller is a server machine that holds replicas of a subset of a directory's naming contexts (NCs).
domain local group
A domain local group can be used on access-control lists (ACLs) only in its own domain. A domain local group can contain users and global groups from any domain in the forest, universal groups, and other domain local groups in its own domain.
domain name service (DNS)
A domain name service (DNS) is used in Internet routing to convert an IP address to a friendlier text address.
domain policy
See policy-based administration.


E

enforcement point
An enforcement point is a discrete location in the network where the policies selected at a decision point are enforced. Every enforcement point is associated with a single decision point. Enforcement points and decision points are often co-located, but this need not be the case. For example, network bandwidth allocation policy is selected by a subnet bandwidth manager and enforced by the network devices (switches and routers) on the subnet in question. The decision point is in the subnet bandwidth manager, and the enforcement points are in the routers and switches.
enumerator
An enumerator is an object that supports the IEnumVARIANT interface, which provides a Next method to enumerate objects in a collection.


F

forest
A forest is a set of one or more trees that do not form a contiguous namespace. All trees in a forest share a common schema, configuration, and global catalog. All trees in a given forest trust each other using transitive hierarchical Kerberos trust relationships. Unlike trees, a forest does not need a distinct name. A forest exists as a set of cross-reference objects and Kerberos trust relationships known to the member trees. Trees in a forest form a hierarchy for the purposes of Kerberos trust; the tree name at the root of the trust tree can be used to refer to a given forest.
fully qualified domain name (FDN)
A fully qualified domain name is a domain name that uses the following form: CN=John Smith,OU=Users,DC=Engineering,DC=Widget,DC=Com


G

global catalog (GC)
Active Directory forest consists of several partitions. Often, the user or application does not know what partition contains a desired object. The global catalog (GC) servers enable users and applications to find an object in a forest, given one or more attributes of the target object.

A global catalog server is a domain controller that contains a full replica of its own domain and a partial replica of every other domain in the forest. Like all domain controllers, it contains the schema and configuration naming contexts as well. This means that a GC server holds a replica of every object in Active Directory, but most replicas contain only a small subset of attributes. The attributes in the partial replicas are those most frequently used in search operations (such as a user's first and last names, login names, and so on) — attributes that are most useful to locate a full replica of the object. A GC search yields the distinguished name (DN) of the desired object; given the DN, an application can connect to a domain controller holding a full replica of the object.

Administrators specify which domain controllers are global catalog servers. The Active Directory replication system automatically maintains partial replicas held on global catalog servers. The properties replicated into the global catalog include a base set defined by Microsoft. Administrators can specify additional properties to meet the needs of their installation.

global catalog server
The global catalog server is a Windows 2000 domain controller that holds a copy of the global catalog for the forest.
global group
A global group can appear on access-control lists (ACLs) anywhere in the forest. A global group can contain users and other global groups from its own domain.
group
The three group types (global, domain local, universal) provide a rich and flexible access-control environment, while reducing replication traffic to the global catalog (GC) when group membership changes. A universal group appears in the GC, but will contain primarily global groups from domains in the forest. Once the global groups are established, the membership in the universal group will change infrequently. Global groups appear in the GC, but not their members. Membership changes in global groups are not replicated outside of the domain where they are defined. Domain local groups are valid only in the domain where they are defined and do not appear in the GC at all.
group policy
Group policy is an extensible framework that refers to applying policy to the "groups" of computers and/or users contained within Active Directory containers. This type of policy includes not only registry-based policies, but many types of policy data, such as file deployment, application deployment, logon/logoff scripts and startup/shutdown scripts, domain security, IPSEC, and so on. The "blobs of policy" are referred to as group policy objects (GPO).

This new infrastructure works with a document-centric approach. In addition to the enhanced polices that are available, the directory hierarchy allows group policy to affect computers and users in sites, domains, or organizational units (SDOU), as well as filtering effective policy based on security group membership. GPO(s) are associated (linked) with these Active Directory containers: sites, domains, organizational units (SDOU). This is analogous to Microsoft® Word permitting multiple .dot templates to specify the formatting of a .doc file.

Group Policy Editor
The Group Policy Editor is a tool that is used for configuring policy. The Group Policy Editor (GPE) has a relationship to group policy objects (GPO) that is similar to the one that Microsoft Word has to .dot (template) files.
group policy object
A group policy object (GPO) is a virtual collection of policies. It is given a unique name, such as a GUID. GPOs store group policy settings in two locations: a group policy container (GPC) (preferred) and a group policy template (GPT). The group policy container is an Active Directory object that stores version information, status information and other policy information. The group policy template is used for file-based data, and stores software policy, script, and deployment information. The group policy template is located on the system volume folder of the domain controller.

A GPO can be associated with one or more Active Directory containers, such as a site, domain, or organizational unit. Multiple containers can be associated with the same GPO, and a single container can have more than one associated GPO.

In addition, by default every computer receives a local group policy object (LGPO) that contains only policies specifically for security. It is also possible for the administrator to set and apply different local group policies on individual computers. This is useful for computers that are not members of a domain, or computers that the administrator wishes to exempt from group policy inherited from the domain.


I

IETF
IETF is the Internet Engineering Task Force. See www.ietf.org for information on the IETF and its activities.
Inheritance
Inheritance is the ability to build new object classes from existing object classes. The new object is defined as a subclass of the original object. The original object becomes a superclass of the new object. A subclass inherits the attributes of the superclass, including structure rules and content rules. Inheritance is also recursive. For example, contact is a subclass of organizationalPerson; organizationalPerson is a subclass of person; and person is a subclass of Top. So, the contact class inherits all the attributes from the organizationalPerson, person, and Top classes.

All object classes in the schema are derived from the special class Top. With the exception of Top, all object classes are subclasses of some other object class.

interface
In ADSI, interface refers to a COM interface. Each COM interface defines a set of related member functions that provide access to COM objects. ADSI defines and implements a set of COM interfaces and related APIs for providing access to directory service objects.
ISM
ISM is the Intersite Messaging Service. It supports pluggable transports for asynchronous, site-to-site messaging. Each transport serves two major roles: to send and receive messages, and to make topology queries (such as what are the various sites connected by this transport, and at what cost?).

Two Intersite Messaging Services are shipped in Windows 2000, remote procedure call (RPC), and simple mail transfer protocol (SMTP).


L

latency
Latency is an intrinsic characteristic of Active Directory replication. Latency is the delay between the time an update is applied to a given replica and the time that the update is propagated to some other replica. Latency is sometimes referred to as propagation delay.

Background

The replication model used in Active Directory is called multi-master loose consistency with convergence. In this model, the directory can have many replicas; a replication system propagates changes made at any given replica to all other replicas. The replicas are not guaranteed to be consistent with each other at any particular point in time ("loose consistency"), since changes can be applied to any replica at any time ("multi-master"). If the system is allowed to reach a steady state, in which no new updates are occurring and all previous updates have been completely replicated, all replicas are guaranteed to converge on the same set of values ("convergence").

LDAP
LDAP is the Lightweight Directory Access Protocol. The standard Internet communications protocol used to communicate with the Windows NT® directory service. Both versions 2 and 3 of LDAP are supported.
local group
As in earlier versions of Windows NT, administrators on member servers and workstations can create local groups. These remain strictly local to the machine where they are created, and do not appear in the directory.

Windows® 2000 introduces the domain local group which does appear in the directory. Domain local groups provide the functionality of local groups defined on domain controllers in earlier versions of Windows NT.


M

MAPI
MAPI is the messaging API. Active Directory provides MAPI support for backwards compatibility with Exchange applications. New applications should use ADSI for accessing the directory.
Microsoft® Management Console (MMC)
The Microsoft Management Console is the Windows® 2000 user interface for managing networks.
mixed mode
Windows 2000 domains are installed in mixed mode, by default. In mixed mode, the domain may have Windows NT® 4.0 backup domain controllers present. Nested groups are not supported in mixed mode.


N

name resolution
Name resolution is the process of translating a name into some object or information that the name represents.
namespace
A namespace is a directory service that uses a particular syntax for naming conventions. Examples are:

Windows NT

"//MyWorkstation/MyName"

LDAP

"//MyServer/C=US/O=MS/CN=MyName"

NDS

"//Planets/O=Mars/OU=DEV"

Novell NetWare® 3.x

"//Docs/MyNw3xPrinter"

naming context (NC)
The naming context is a contiguous subtree of the directory. In Active Directory the naming context is the unit of partitioning for replication purposes. Each domain is a naming context.

Active Directory is made up of one or more naming contexts or partitions. A naming context is any contiguous subtree of the directory. Naming contexts are the unit of replication. In Active Directory, a single server always holds at least three naming contexts:

  • The schema
  • The configuration (replication topology and related metadata)
  • One or more user naming contexts (subtrees containing the actual objects in the directory)
native mode
A Windows® 2000 Domain is in native mode when:
  • All domain controllers in the domain have been upgraded to Windows® 2000
  • An administrator has enabled the native mode operation using the domain property page in the Active Directory Users and Computers snap-in

    Domains must be operating in native mode for nested groups to be supported.

NDS
NDS refers to NetWare Directory Services.
non-authoritative restore
A non-authoritative restore is a restore of a backup copy of a Window® 2000 domain controller in which the objects in the restored directory are not treated as authoritative. This means that the resorted objects are updated with changes held in other replicas of the restored domain.


O

object
In ADSI, an object refers to a COM object that implements one or more interfaces. For more information, see The Component Object Model.

An object is the basic named unit of storage in a directory service. A directory service object is defined in the schema for a directory service by the class object and the group of attribute objects referenced by the class object.


object class

Microsoft Active Directory SDK
object class
The object class is the formal definition of a specific kind of object that can be stored in the directory. An object class is a distinct, named set of attributes that represents something concrete, such as a user, a printer, or an application. The attributes hold data describing the thing that is identified by the directory object. Attributes of a user might include the user's given name, surname, and e-mail address. The terms object class and class are used interchangeably. The attributes that can be used to describe an object are determined by the content rules.


object class instance

Microsoft Active Directory SDK
object class instance
An object class instance is an object with a given object class. This term is used to distinguish between the definition of a class and a discrete occurrence of the class. For example, storing a user object for "James Smith" in the directory service creates an instance of user. Typically, you'd just say "object" rather than "object class instance."
object identifier (OID)
An object identifier (OID) is a number that unambiguously identifies an object class or attribute and the syntax in a directory service. Issuing authorities provide OIDs. An OID is represented as a dotted decimal string (for example, "1.2.3.4"). Enterprises (and individuals) can obtain a root OID from an issuing authority and use it to allocate additional OIDs.

OIDs have other uses as well, see Obtaining a Root Object Identifier (OID).

object policy
An object is an entity with which the subject wants to interact in some way that is governed by a policy. For example, when a user running an IP Telephone application (the subject), attempts to reserve some network bandwidth (the object), a policy determines whether the request will be granted.


OID

Microsoft Active Directory SDK
OID
See object identifier.
operation policy
An operation is the interaction that a subject wants to have with an object. For example, when a user (the subject), wants to access (the operation), a given server (the object), over the network, a policy determines whether that access will be allowed.


operational attribute

Microsoft Active Directory SDK
operational attribute
An operation attribute is an attribute implemented internally by a particular directory implementation. Operational attributes do not appear in the schema and must be requested explicitly. Operational Attributes occurred originally in the X.500 specifications for a directory service and have been carried over into the LDAP version 3 specifications (RFC 2251). RFC 2251 requires support for certain operational attributes; a given directory implementation may implement any number of others.


P

partial update
A partial update occurs when applications read the same set of objects from different replicas while replication is in progress. Applications at the remote replica see some of the changes, but not all. Note that there is a small window in which partial update can affect an application: the application must start reading objects while inbound replication is in progress, after one or more of the related, changed objects have been received but before all have been received. The time between the updates at the source replica directly affects the size of this window.

Updates that occur close together in time will be replicated close together in time. Partial update is an issue when an application uses a related set of objects.

For example, a remote access service can use the directory to store policy and profile information. The policy information is stored in one set of objects, and the profile in another set. When a user connects to the remote access service, the remote access service reads the policy to determine whether the user is allowed to connect, and if so, what profile to apply to the user's session.

policy
A policy is the set of rules that govern the interaction between a subject and an object. For example, when an IP security agent (the subject), starts on a given computer (the object), a policy determines how that computer will participate in secure IP connections.
policy-based administration
The goal of policy-based administration is to give administrator's the ability to state a wish about the state of their users and computer environment once, and then rely on the system to enforce that wish"

The concepts are simple. Policies express business rules. Commonly used policies in distributed systems define how resources in the network are accessed, configured, and used. For example, when a user logs on, policies determine what applications they see on their desktop. When a service (for example, SQL Server) starts on a machine, policies determine how many users can connect to the service. When an employee moves to the engineering group, policies determine which documents and other services the employee can access. Since a single policy can be applied to many users, machines, or services, the administrator manages a small number of policies, rather than a large number of computers.

policy engine
A policy engine is software that executes at decision points to perform policy selection, to evaluate conditions and determine what actions must be performed. The concept of the policy engine is quite diffuse; policy engine functionality will often be spread through many parts of the distributed system. For example, Windows® 2000 provides a policy infrastructure that includes a policy store (group policy objects), a policy engine that runs as part of user logon (WinLogon), and an API for services to invoke the policy selection process on demand (GetGPO List). Some applications and services will use WinLogon integration to apply their policies to users; other will use the GetGPOList function to implement their own policy decision and enforcement points.
policy selection
Policy selection is the mechanism that determines what policies should be applied to a given operation. Specifically, policy selection is the process used at a decision point to choose the policies that must be evaluated for a given subject, object, and operation from among those available.
primary domain controller (PDC)
A primary domain controller is a computer that runs a Windows NT Server that authenticates logons and maintains the directory database for a Windows NT® 4.0 domain. A PDC is not used in Windows® 2000. Instead, one domain controller is designated as the PDC for backwards compatibility.
profile (policy)
A profile is a collection of information selected and applied to the interaction between a subject and an object by an action that is the outcome of evaluation of policy conditions. The content of a profile is specific to the subjects and objects in question.

Profiles can further simplify administration by reducing the total number of policies. For example, a given server application may have a large number of configuration parameters. A policy for that application can reference the profile; this is simpler than using multiple policies to accomplish the same thing.

property
A property is a value that can be read or written. Properties are associated with specific interfaces on an object.
provider
A provider is a vendor who supplies an implementation of the ADSI objects for a particular namespace.


Q

query
A query is a request to a directory service to return the location of one or more specified directory service objects.


R

referral
A referral is returned to an LDAP client by a directory system agent (DSA) when the query presented by the client cannot be serviced locally, and the DSA has "knowledge information" about other DSAs that can handle the query.
registry
The registry is a database in which configuration information is registered. This database takes the place of most configuration and initialization files for Windows® and new Windows-based applications.
remote procedure call (RPC)
System for calling software procedures on a different machine using a network. For more information, see Microsoft RPC Model.
replication
Active Directory provides multi-master replication. Multi-master replication means that all replicas of a given partition are writeable. This allows updates to be applied to any replica of a given partition. Active Directory replication system propagates the changes from a given replica to all other replicas. Replication is automatic and transparent.
RPC
See remote procedure call.


S

SACL
See system access-control list.
schema
The schema is a formal definition of every object class that can be that can be used in defining object classes. In ADSI, the schema management interfaces supply a means of defining types of COM objects known as schema classes, their properties, and the syntaxes those properties use.
scope
The scope of Active Directory is large. It can include every single object (printer, file, or user), every server, and every domain in a single wide area network. It can also include several wide area networks combined. It is important to keep in mind that Active Directory can scale from a single computer, to a single computer network, to many computer networks combined.
security identifier (SID)
The security identifier identifies what principal (user or group) to whom an access control entry (ACE) applies.
service principal name (SPN)
A service principal name is a "friendly" name for a service, shorter than the distinguished name (DN), and easier to remember.
site
A site is a location in a network where Active Directory servers are held. A site is defined as one or more well connected TCP/IP subnets. "Well connected" means that network connectivity is highly reliable and fast. Defining a site as a set of subnets allows administrators to quickly and easily configure Active Directory access and replication topology to take advantage of the physical network. When users log in, Active Directory clients find Active Directory servers in the same site as the user. Since machines in the same site are close to each other in network terms, communication among machines is reliable, fast, and efficient.

Determining the local site at login time is easy because the user's workstation already knows what TCP/IP subnet it is on, and subnets translate directly to Active Directory sites.

SMTP
See simple mail transfer protocol.
store
The physical storage for each replica of Active Directory is called the store. The store is implemented using the extensible storage engine (ESE), formerly known as "Jet Blue".


structure rules

Microsoft Active Directory SDK
structure rules
Structure rules define the possible tree structure. In Active Directory, the structure rules are completely expressed by the possSuperiors attribute present on each classSchema object.
subject (policy)
A subject is an entity to which a given policy applies. Users, computers, services, and so on, are all potential subjects. For example, when SQL Server (the subject), starts up on a given computer system (the object), a policy controlling the maximum number of database connections can be applied.
system access-control list (SACL)
The system access-control list controls the generation of audit messages for attempts to access a securable object. The ability to get or set an object's SACL is controlled by a privilege typically held only by system administrators.


T

Top
In the Active Directory schema, Top is the object class from which all other object classes are ultimately derived.
tree
A tree is a hierarchy of objects and containers. Endpoints on the tree are usually objects. Nodes or branches are containers. A tree shows how objects are connected, or the path from one object to another. A simple directory is a container. A computer network or domain is also a container. A contiguous subtree is any unbroken path in the tree, including all the members of any container in that path.
trustee
A trustee is a user with access rights to an object.


U

universal group
A universal group is the simplest form of group. Universal groups can appear in access-control lists (ACLs) anywhere in the forest, and can contain other universal groups, global groups, and users from anywhere in the forest. Small installations can use universal groups exclusively and not concern themselves with global and local groups.
update sequence number (USN)
An update sequence number is a 64-bit number that each Active Directory server maintains. It is used instead of a timestamp to keep track of replication updates.
user principal name (UPN)
A user principal name is a "friendly" name for a user or group, shorter than the distinguished name, and easier to remember. It is composed of a shorthand name for the user and the domain name service (DNS) name of the tree where the user object resides. For example, someone in the microsoft.com tree might have the UPN someone@microsoft.com.


V

virtual container
Active Directory allows other directories to be exposed using virtual containers. A virtual container allows any LDAP-compliant directory to be accessed transparently using Active Directory. The virtual container is implemented using "knowledge information" stored in Active Directory. The knowledge information describes where in Active Directory the foreign directory should appear, and contains the domain name service (DNS) name of a server holding a copy of the foreign directory and the distinguished name at which to begin search operations in the foreign domain.


X

X.500
X.500 is a set of standards that define a distributed directory service, developed by the International Organization for Standardization (ISO).


Requirements

Many topics that introduce the syntax of an API element contain information about the build and run-time requirements for using the element. The format is as follows:

Windows NT/2000: Requires Windows NT 3.1 or later.
Windows 95/98: Requires Windows 95 or later.
Windows CE: Requires version 1.0 or later.
Header: Declared in winuser.h.
Import Library: Use user32.lib.
Unicode: Implemented as Unicode and ANSI versions on Windows NT/2000.

The first line indicates the versions of Windows NT on which an element is supported. This information may include required service packs, option packs, or components such as Internet Explorer. If an element is not supported, the line reads "Windows NT/2000: Unsupported."

The second line indicates whether an element is supported on Windows 95 or Windows 98. This information may include required OEM service releases, redistributables, or components such as Internet Explorer. If an element is not supported, the line reads "Windows 95/98: Unsupported."

The third line indicates the versions of Windows CE on which an element is supported. If an element is not supported, the line reads "Windows CE: Unsupported."

The fourth line indicates the file that defines an element. This value can be an .h file, .idl file, or .odl file. If the header file is supplied by the user, the line reads "Header: User-defined."

The fifth line indicates the file used to resolve calls to a function or member. This value can be a .lib file, .dll file, or .tlb file. If the import library is supplied by the user, the line reads "Import Library: User-defined." For element types that do not require an import library, this line is omitted.

The sixth line indicates whether an element exists as both a Unicode (W) version and an ANSI (A) version. If Unicode support is not available, this line is omitted.