Introduction
(Download the code for this article at
http://www.agnisoft.com/adsi/adsicode.zip)
Microsoft has released Active
Directory Service Interfaces (ADSI) for Windows 9x, NT and Windows 2000, and now
it's inbuilt in Windows 2000. The Windows 2000 application specification states that you
must use Active Directory when you can, so now you're thinking: How are YOUR applications
going to adapt? What changes do you need to make? How are you going to use
Delphi effectively to make these changes? This is what I intend to address in
this presentation. Let me first introduce a “directory service”. A directory service
is like a telephone directory: if you have a person’s name, you can find
his/her phone number. A directory service keeps track of “resources”, which
could be anything – a file system is a directory that keeps track of files
and folders, an email server is a directory service that indexes users, user
groups etc. There are many directory services already in place: we already
have file systems and email servers. What’s so different? Traditionally,
you’d have to use different API’s (Application Programming Interfaces) to
access different directory services – which :
There is a need to have a common model that every
directory service would support – similar to the ODBC programming model that
all (or most) database vendors now support. A model that will support a hierarchy
of resources, like folders and files, and be simple to use. Active
Directory provides this model. To access an Active Directory, you will use
Active Directory Service Interfaces(ADSI).
Any directory service can choose to publish itself as an Active Directory, so that a common
querying mechanism can be used. At this point the following products support Active Directory:
Active Directory is also an integral part of Windows 2000. In a large organization, Windows NT server
has been used as a Primary or Backup Domain Controller, but it was difficult to integrate
many such controllers to be able to provide security and privileges across the organization.
Active Directory makes it simpler by allowing you to structure your organization into units
which seamlessly (or so they say) integrate with each other. Which also means lesser problems
when you expand, add more servers, more users etc.
To know more about how you can use Active Directory effectively in your organization, please visit
http://www.microsoft.com/windows2000/library/technologies/activedirectory/default.asp
Here's a list of places ADSI would be best used in:
This
presentation assumes readers know COM (Component Object Model) and a
working knowledge of using COM in Delphi. Directory Services and Namespaces
ADSI in a nutshell
ADSI objects are Component Object Model (COM) Objects. You needn’t learn a new
API for ADSI programming. You can develop for ADSI using simple Automation
concepts. All ADSI objects support (and must support) IDispatch, so you can
choose to use Late Binding or Early Binding. (Delphi supports both, quite magnificently)
Here’s a small example of how to create a user on Windows NT 4.0 using Early Binding.
Note: The ADsGetObject function is declared in AdsHlp.pas that is included with this
article. The interface definitions are imported from ActiveDS.tlb in the
WinNT/System32 folder. You will need to install ADSI 2.5 from
http://www.microsoft.com/ntserver/nts/downloads/other/ADSI25/default.asp.
I will explain the architecture in more detail in the Architecture section.
Active Directory Service Interfaces can be used easily in Delphi, though all the examples
and support in the http://www.microsoft.com/adsi
are in Visual Basic or C++ code. With this paper, you will find all the translations of the header files, some
samples and some new Delphi translations. (A copy will be maintained at
http://www.agnisoft.com/adsi)
A Directory Service Provider is a module that gives a user access to a certain directory
service. For instance, we were able to add a user to the Windows NT user manager because Microsoft
has written an ADSI provider for the user manager.
Take a look at the next section for more benefits.
Feature Benefit Open Any directory provider can implement an
Active Directory Service Interfaces provider; users can easily move to a different provider
of the same service with a minimum rewrite. Security ADSI supports both Authentication and
Authorization programming model - You can give even role based security for your
applications. Simple Programming Model As you will see, the COM model is very easy to
understand. The model remains standard for all providers: there's no need to understand vendor
specific APIs. Automation Server Any Automation Controller (for example,
Delphi, Visual Basic, C/C++ and others) can be used to develop directory
service applications. Administrators and developers can use the tools they
already know. Functionally Rich ISVs and sophisticated end users can
develop serious applications using the same Active Directory Service
Interfaces models that are used for simple scripted administrative
applications. Extensible Directory providers, ISVs, and end users
can extend Active Directory Service Interfaces with new objects and functions
to add value or meet unique needs. Introduction
Where is Active Directory used?
"Active Directory" encapsulates all directory services - a list of printers on a
network, a set of services on an NT server etc. Directory services are very useful in an
enterprise where one might know what he wants, may not know what that resource is named.
(like, "give me a list of printers in the 2nd floor").
Active Directory also provides bridges to access other similar directory services, like LDAP,
NDS (Novell Directory Services) etc.
These are just a few applications, and I'm sure there will be more as time goes on.
Prerequisites – What do you
need to know?
What is ADSI?
There could be many objects within a namespace (users in an email server, files in a file system) which need to be
uniquely identifiable by name. But you might have a user in the email server with the same name as a file in the
file system:and they have no idea about each others existance. To maintain uniqueness, object names are prefixed with the name of
the Directory Service they belong to. This name identifies a "namespace", with a namespace
identifier like "WinNT:", "LDAP:" etc. (The ":" means that it's a namespace, and therefore, a directory service)
Object names are prefixed by the namespace identifier, and "//".
Note:This has analogies in the Internet, where you'd have Web pages identified by "http://..." and files on FTP servers
by "ftp://..." etc.
As Microsoft puts it, “ADSI is a set of COM programming interfaces that will make
it easy for customers and Independent Software Vendors (ISVs) to build
applications that register with, access, and manage multiple directory services
with a single set of well-defined interfaces” (Ref.1) Active Directory Service
Interfaces abstract the capabilities of individual directory services: which
means you, as a developer, could access a file system the same way you access
an email server or any other service that supports ADSI.
var
Container : IADsContainer;
NewObject : IADs;
User : IADsUser;
hr : HREsult;
begin
// COM must be initialized
CoInitialize(nil);
// Bind to the container.
hr := ADsGetObject('WinNT://YOURDOMAIN',IADsContainer,Container);
if Failed(hr) then exit;
// Create the new Active Directory Service Interfaces User object.
NewObject := Container.Create('User','ActiveDirectoryUser') as IADs;
// Get the IADsUser interface from the user object.
NewObject.QueryInterface(IID_IADsUser, User);
// Set the password.
User.SetPassword('Borland');
// Complete the operation to create the object.
User.SetInfo;
// Cleanup.
Container._Release;
NewObject._Release;
User._Release;
CoUninitialize;
end;
This doesn't sound very great, you might say. What's the difference between doing this and using
native Windows NT calls to add users? First, this is COM based so if Microsoft decides to change
the entire implementation of the user architecture, your applications are safe, because there will
still be an ADSI provider supporting the same interfaces. (Note: this kind of thing might not be about
to happen.)
Second, consider your gains in extensibility. You can now add a user on a Netware Server
using very similar code, except you'd have to use the ADSI provider for Netware. As we will
see later, the code will follow a similar pattern for doing different activities: creating
a web site, adding an email user etc.
Why should you use ADSI?
The Architecture of
ADSI
This section deals with the core concepts of ADSI. There will be
Most directory services are hierarchical in nature and thus lend themselves to a hierarchical object model. ADSI abstracts this concept by defining Container Interfaces and leaf interfaces. A container object (that implements a Container Interface) will contain zero or more ADSI objects - which could be other containers or leaf objects. As I've said before, access to directory services is through ADSI providers - so each provider is identified by a unique namespace identifier. A provider implements a Namespace object which is a COM Object that is a one-stop-shop: you can access any object in the namespace through this object.
These namespace objects are stored in an Active Directory
Namespace Container object which is identified by the name "ADS:". (See Figure).
Each Namespace object is itself a container - it contains the root nodes of the
directory service objects. Every container object and leaf object support a common
set of methods - so that users can interact with it uniformly. These common methods are
part of the IADs interface for all nodes, and IADsContainer
interface for container nodes.
These common methods do not expose all the functionalities of a provider - simply because
the domain of a "directory service provider" is too large to be able to abstract everything.
To allow providers to extend functionality, ADSI supplies a schema model that I will describe
later.
There are many ADSI Interfaces that have been defined for specific purposes. Here is a list:
IADs |
Object Identification |
IADsContainer |
Object Lifetime Management and Detection
|
IADsPropertyList |
Object Property Management
|
IDirectoryObject |
Direct Object Access
|
IUnknown |
COM Object Management
|
IDispatch |
Type Library Information and Method Invocation
|
As an analogy, consider any Delphi application. There's a global object called Screen that
contains information about all the forms created(Screen.Forms). Each form has a set of standard properties
(Name, Tag for example).
Each form could contain many components in it, each of which has the standard properties (Name, Tag) at least.
So a Form is analogous to a "container" in ADSI. In congruence, Screen is also a container.
Taking this further, a "Form" is a namespace - Any object in this namespace is derived from TForm and thus
supports everything that TForm does. One more thing: ADSI requires that any object has a unique name:
if we assume that all forms had unique names (which they usually will), then I could identify any object (Form )
by using its name. If I had to register this globally - I would prefix the name with "Form:". (Form:MainForm for instance).
So "Form:" is handled by the Screen object which figures out where "Mainform" is. (You could have other namespaces handled
by other containers)
To translate this to ADSI, we would have to have TForm support :
If this was implemented, you can create any form by calling Screen.OpenDSObject('Form:MainForm');.
With ADSI, there are libraries present so that you can open an object directly, given its path. You don't
need to create the namespace container in order to create an object. A few functions here are:
In this section I will talk about how you can use ADSI in Delphi. I've used a number of samples from the Windows Platform SDK as a reference. If you take a look at this, most examples you will find will use late binding : Visual Basic code or VBScript/ASP code. I'll use a combination of Late and Early Binding - Delphi can use both - to demonstrate various features.
1. WinNT: - The Windows NT provider for Windows NT 4.0 (and Windows 2000) domain controllers.
2. LDAP: - For communication with LDAP servers like Exchange Server 5.5.
3. NDS: - Provider for Netware Directory Services
(The initial elements of the ADsPath string are the namespace identifier (progID) of the ADSI provider, followed by "//",
followed by whatever syntax is dictated by the provider namespace)
With this information at hand, lets consider a few ADsPaths that could identify objects.
1. WinNT://MyDomain/Adminstrator - identifies the Administrator user on MyDomain, a Windows NT domain.
2. LDAP://EXCHSVR/CN=info,DC=AGNISOFT,DC=COM - the info@agnisoft.com account on EXCHSVR
To find all the providers installed on your machine, you can enumerate the namespaces in "ADs:". Here is some Visual Basic code that does it:
Set x = GetObject("ADs:") For Each provider In x provider.Name Next |
var x : IADsContainer; e : IEnumVariant; hr, i : integer; varArr : OleVariant; lNumElements : ULONG; item : IADs; begin hr := ADsGetObject( 'ADs:', IID_IADsContainer, x); // bind to the object hr := ADsBuildEnumerator(x,e); // start enumerating while SUCCEEDED(hr) do begin // get the next contained object hr := ADsEnumerateNext(e,1,varArr,lNumElements); if (lNumElements<=0) then // are we done? break; //varArr contains an IDispatch pointer to the contained object. IDispatch(varArr).QueryInterface(IADs, item) ; ShowMessage(item.ADsPath); end; if e<>nil then hr := ADsFreeEnumerator(e); end; |
This code is a bit complex and troublesome to do everytime. I've added a funtion that allows easy enumeration.
procedure ADsEnumerateObjects(Container : IADsContainer; Func : TADsEnumCallback); var e : IEnumVARIANT; varArr : OleVariant; lNumElements : ULong; obj : IADs; hr : integer; begin hr := ADsBuildEnumerator(Container,e); while(Succeeded(Hr)) do begin hr := ADsEnumerateNext(e,1, varArr ,lNumElements); if (lNumElements=0) then break; IDispatch(varArr).QueryInterface(IADs, obj); if obj<>nil then begin Func(obj); end; varArr := NULL; end; // do not call ADsFreeEnumerator(e); since e will be released by Delphi end; |
You can use this in a Delphi form like so:
procedure TForm1.Button2Click(Sender: TObject); begin ADsEnumerateObjects('ADs:', Callback); end; procedure TForm1.Callback(Obj: IADs); var s : string; begin ShowMessage(Obj.name); end; |
hr := ADsOpenObject('IIS://localhost', 'Admin','Admin', ADS_SECURE_AUTHENTICATION , IADs, obj ); |
Once Binding is established, you will want to get or set properties of the object. The steps involved are:
a) Bind to the object
b) Set properties
c) Call SetInfo
d) Release the object
(b) and (c) are required because ADSI caches the properties on the client side and updates the server only when you call SetInfo. (Saves a lot of network traffic this way).
Here's an example.
// Late Binding var obj : Variant; begin obj := ADsHlp.GetObject('WinNT://AGNISOFT/Deepak'); // bind to the object obj.Put( 'FullName', 'Deepak'); // set properties obj.SetInfo; obj := NULL; // release the object end; |
Properties could also have multiple values, like additional phone numbers. You would use the PutEx and GetEx functions to set or access these properties.
var obj : IAds; begin ADsGetObject('LDAP://CN=deepak,CN=Users,DC=AGNISOFT,DC=COM', IAds, Obj); // assume there was '111-1111' and '222-2222' obj.PutEx(ADS_PROPERTY_APPEND, 'otherHomePhone', VarArrayOf(['333-3333']) ); obj.SetInfo; //now there will be '111-1111', //'222-2222' and '333-3333' obj.PutEx(ADS_PROPERTY_DELETE, 'otherHomePhone', VarArrayOf(['111-1111', '222-2222'])); obj.SetInfo; //now there will be only '333-3333' obj.PutEx(ADS_PROPERTY_UPDATE, 'otherHomePhone', VarArrayOf(['888-8888','999-9999'])); obj.SetInfo; //now there will be '888-8888' //and '999-9999' obj.PutEx(ADS_PROPERTY_CLEAR, 'otherHomePhone', NULL); obj.SetInfo;//now there will be nothing end; |
1. Using ADO
Active Data Objects is Microsoft's latest Database Access solution. It works with OLE DB providers, and ADSI comes with a provider named "ADsDSOObject". In Delphi, all you need to do is to drop a TADOConnection and set its provider to "ADsDSOObject". Then, drop a TADOQuery, connect it to the TADOConnection and query ADSI - this is a method for simple searching.
Here's how I've got all the users, their user names and their last names from my machine's Active Directory.
The SQL syntax is :
SELECT [ALL] select-list FROM 'ADsPath' [WHERE search-condition] [ORDER BY sort-list]
The Query I have used in the form is
SELECT AdsPath, CN, SN FROM 'LDAP://DC=AGNISOFT,DC=COM' WHERE objectClass='user' ORDER BY sn |
The ADSI provider is read-only at this time. Microsoft plans to ship a read-write provider in future.
To modify data, you can:
1. Get the ADsPath from the Query
2. Use ADsGetObject to bind to the ADsPath
3. Get/Set properties
At this point, only the LDAP and the NDS providers support searching using ADO. You cannot
use ADO to search for user in a Windows NT (Or 2000) domain.
2. Using COM Interfaces - IDirectorySearch
If you don't want to use ADO, you can use the IDirectorySearch Interface. The steps involved are:
1. Bind to the object
2. Call QueryInterface on the object for IDirectorySearch
3. Call IDirectorySearch.ExecuteSearch, passing the search query and get a search handle
4. Call IDirectorySearch.GetNextRow and for each row, call IDirectorySearch.GetColumn(columnName) to get the columns.
// bind to the object AdsGetObject(edtObjectPath.Text, IDirectorySearch, search); try // set parameters opt[0].dwSearchPref := ADS_SEARCHPREF_SEARCH_SCOPE; opt[0].vValue.dwType := ADSTYPE_INTEGER; opt[0].vValue.Integer := ADS_SCOPE_SUBTREE; search.SetSearchPreference(@opt[0],1); // search p[0] := StringToOleStr('Name'); search.ExecuteSearch('(objectCategory=Group)',@p[0], 1, ptrResult); // get records hr := search.GetNextRow(ptrResult); while (hr <> S_ADS_NOMORE_ROWS) do begin hr := search.GetColumn(ptrResult, p[0],col); if Succeeded(hr) then begin ShowMessage(col.pAdsvalues^.CaseIgnoreString); search.FreeColumn(col); end; Hr := search.GetNextRow(ptrResult); end; search.CloseSearchHandle(ptrResult); finally // free memory search._Release; end; |
Example:
hr := ADsOpenObject('IIS://localhost', 'testuser','pwd',
ADS_SECURE_AUTHENTICATION , IADs, obj );
ADS_SECURE_AUTHENTICATION specifies that Kerberos or NTLM is used to authenticate the password. You can even specify password encryption (if your server supports it).
Role based security to properties - You might need to secure ADSI itself - control who can read/write certain properties etc. The properties of ADSI are controlled by Access Control Entries (ACEs) in Windows 2000. You can create an ACE and add it to the Discretionary Access Control List (DACL) of the SecurityDescriptor of an object. ( Use obj.Get('ntSecurityDescriptor') to get the security descriptor) The ACE can allow or deny access to one or all properties of an object. The security is inherited - so if you change the access control list of a container, all its descendants will inherit it.
1. If your application will use a known directory service, you must use ADSI to get or set properties in the
Active Directory service. For instance, if you need to add or modify a user, you must use ADSI to do so.
2. In a client-server or multi-tier application, you are suggested to publish server attributes in the Active Directory.
Which means that the client applications should be able to use ADSI to get all the information about the server
application.
To do this, you need to be able to extend ADSI. I'll talk about two ways you could extend ADSI. But first, lets see how the ADSI Schema works.
The predefined ADSI objects have very few properties: these may not be enough for a particular provider. So, ADSI allows your provider to extend the basic interface by adding properties to objects within your namespace. The schema objects are special ADSI objects : They allow you to :
- Browse the definition of objects
- Extend the definition of objects
The schema object contains definitions of :
1. What kind of objects will be present (eg. in WinNT:, you have Users, Groups, Services etc),
2. What properties these objects will have (For Users in WinNT:, FullName, Description etc. are properties) and
which properties are mandatory and which are optional.
3. The syntax of these properties (FullName is a String, UserFlags is an integer etc.)
(1) above is represented by a Class Object.
(2) by a Property Object
(3) by a Syntax object
These three are placed in the Active Directory as follows:
To browse the schema of an object, you must:
1. Get the ADsPath of the schema - The IADs.Get_Schema call does the job
2. create the schema object and browse it.
Here's an example that gets the list of Mandatory and optional properties of all Users in the LDAP namespace.
var obj : IAds; s : WideString; cls : IADsClass; cont : IADsContainer; i : integer; begin AdsGetObject('LDAP://CN=Users,DC=AGNISOFT,DC=COM', IADs, obj ); s := obj.Get_Schema; AdsGetObject(s, IADsClass, cls ); if VarIsArray(cls.MandatoryProperties) then begin for i := VarArrayLowBound(cls.MandatoryProperties,1) to VarArrayHighBound(cls.MandatoryProperties,1) do begin s := cls.MandatoryProperties[i]; ShowMessage('MANDATORY:' + s); end; end; if VarIsArray(cls.OptionalProperties) then begin for i := VarArrayLowBound(cls.OptionalProperties,1) to VarArrayHighBound(cls.OptionalProperties,1) do begin s := cls.OptionalProperties[i]; ShowMessage('Optional:' + s); end; end; |
Anyone that needs to extend ADSI can do so by writing an extension - which is nothing but a COM object. A backup vendor, for instance, could write an extension that supports "Backup" and "Restore" functions that extend the IADsComputer interface - This would be useful for administrator to write scripts for automatically backing up computers to a tape drive.
This seems like quite a big task, and though there is a sample in the Windows 2000 platform SDK, it isn't quite easy. I have written a sample in order to make it easier for you to begin - it's available along with this article. Please check www.agnisoft.com/adsi for updates.
There will be a lot of focus on ADSI in the future - you will see a number of components that will give you easier access to ADSI objects. You can begin to perform administrative tasks using ADSI, and identify areas of your applications where it will be better to use ADSI rather than a native Directory provider.
Active Directory is not something to be ignored because it forms a part of the most recent operating system that you will support - Windows 2000.