Active Directory Service Interfaces (ADSI) Benefits
Active Directory Services Interface (ADSI) is a set of COM (Common Object Model) programming Interfaces. Like ODBC, ADSI provides common access to directories by adding a provider for each directory protocol type. Windows contains providers for:
- WinNT - access to Windows NT 3.51 and Windows NT4;
- LDAP - LDAP directories including Windows Active Directory, Site Server, Microsoft Exchange and third party LDAP servers;
- NDS - Novell NDS.
Benefits of accessing directories with ADSI:
- Open Architecture - Any directory provider can implement an ADSI interface;
- Directory Service Independent - Applications are not bound to a vendor's proprietary directory service since it is using an API;
- Security - ADSI supports authentication.
Adsi includes a comprehensive set of powerfull classes, to enable (remote) administration, including:
- ADSI Create a DS object samples
- ADSI Write a DS object samples
- ADSI Delete a DS object samples
- ADSI Read a DS object samples
- ADSI Miscellaneous samples
ADSI objects are COM objects, which represent objects in an underlying directory service. Objects can be container objects (like Folders) or Leaf objects (like Files). Each object has a unique ADSI path - a provider name followed by an object path. ADSI provides an abstract schema which describes the type of objects and attributes supported by each provider. Objects are read into cache when GetInfo or GetObject are called. Changes reside in cached memory on the client until a SetInfo is issued. SetInfo writes data back to the underlying directory store.
LDAP provider and serverless binding
The preferred method for connecting to an object is to use serverless binding; this means that the server is not explicitly provided; the default domain controller is the source of the LDAP requests. If the requested operations cannot be serviced in the local domain, a referral to the correct server is generated when possible, and the closest server is given. A serverless path is of the form LDAP://object. To bind to the domain DNS object which is the root container of the domain naming context:
Set Odse = GetObject( "LDAP//DC=corp,DC=Microsoft,DC=com")
The RootDse is a special LDAP object that exists on all LDAP v3 servers. With it you can write scripts that are independent of the domain or enterprise on whih they are run:
Set Odse = GetObject( "LDAP://RootDse" )
For example, to reference an object in the current domain you can read the value for DefaultNamingContext to determine the current domain:
' Get path to the configuration naming context. Set RootDse = GetObject( "LDAP//RootDse" ) Path = "LDAP://" & RootDse.get( "DefaultNamingContext" )
Domain-based information such as users, groups, and computers resides in the domain naming context. Enterprise configuration information such as sites and subnets can be found in the Configuration Naming context. You can learn the path to naming contexts by examining ConfigurationNamingContext and SchemaNamingContext.
Set Odse = GetObject( "LDAP//servername/RootDse" )
Using the Global Catalog
A global catalog (GC) server is a domain controller that contains a partial read-only replica of every object in every naming context. The replica is used to quickly search the enterprise for an object. The GC contains all objects from all naming contexts, but it is partial in that it contains only attributes designated for replication to the GC. The GC is accessed using port 3268 or by the GC provider as alias. In ADSI any reference to the GC is mapped to the LDAP provider on port 3268. Some of the common uses for searching the GC are:
- Finding user's address book information;
- Looking up members of a universal group;
- Mapping the User Principal Name to a specific User Account.
Reading an object
To read an object you must first use GetObject to bind to it:
Set objUser = GetObject( "LDAP://CN=wjohnson;CN=Users;DC=klm,DC=com" )
This fills the object cache in the client's memory with the object's attributes. You can access each attribute by it's name:
WScript.Echo objUser.givenName & " " & objUser.sn & " " & objUser.mail Set objUser = Nothing
Updating an object
To update attributes on an existing object, you have to first bind to the object with GetObject, set each attribute's value (to update the values in the local object cache on the client), then issue a SetInfo to write the changes back to the directory. This example sets the mail address and the user's last name on user account:
Set objUser = GetObject( "LDAP://CN=wjohnson,CN=User,DC=klm,DC=com" ) objUser.mail = "firstname.lastname@example.org" objUser.sn = "wjohnson" objUser.SetInfo Set objUser = Nothing
Enumerating a container
To enumerate a container such as on OU, first bind to the OU and then use a loop to enumerate the container's object. To list all objects in the Users container:
Set ou = GetObject( "LDAP://CN=Users,DC=klm,DC=com" ) For each obj in ou WScript.Echo obj.Name Next
OLE DB provides a common way to query for information in a database-like way. The ADSI OLE DB provider allows you to use ActiveX Data Object (ADOs) to search the Active Directory. The provider is read-only, so if you need to modify an object after you search for it, use GetObject with the AdsPath
To search the Active Directory you must first create the ADO connection and command objects:
' list all objects in the domain naming context Dim con, rs, Com Set con = WScript.CreateObject( "ADODB.Connection" ) Set com = WScript.CreateObject( "ADODB.Command" )
Next, open the ADO provider:
' Open a connection object con.Provider = "ADsDSObject" con.Open "Active Directory Provider"
Then set the command object to use the current connection object, and build the query using either a simple SQL format or the LDAP search filter format (used in the example below). The query specifies the search's starting path and a filter for matching (the sample below looks for any type of object). Then list the attributes you want the query to return. Finally, specify the depth of the search: subtree for a deep search, base for a single object, or one level for searching a container.
Set Com.ActiveConnection = con Com.CommandText = "<GC://DC=hq,DC=klm,DC=com>;(objectClass=*);adspath;subtree" ' Set the preferences for Search Com.Properties( "Page Size" ) = 512 Com.Properties( "TimeOut" ) = 30 ' seconds
When you execute the query, the results are retuned in a recordset. Loop through the recordset and print out the value, then move on to the next record.
While Not rs.EOF WScript.Echo rs.Fields( "AdsPath").Value Rs.MoveNext Wend
Managing Domain Information
Extracting Computer Information
Network Administrators have always wanted an easy way to get a list of network workstations along with operating system and service pack information. You can now do this by using new attributes on Windows computer accounts to identify the computer's current status. The computer object is now automatically updated with information (from the netlogon service during secure channel setup) about the client's operating system, operating system version, and service pack level.
You can identify unused or possibly inactive computer accounts; accounts that have never been used do not have the operating system and version attributes set. If the whenChanged attribute is more than a month old, the computer probably is not active on a network making periodic password changes. The whenChanged attribute is a non-replicated attribute which means it is calculated on each DC. The lastLogon attribute is not replicated between DCs; to determine the last logon time you have to examine it on all DCs.
Attrinbutes of interest:
- Name - Computer name
- dnsHostName - Full DNS hostname of the machine;
- UserAccountControl - Type of account: ADS_UF_WORKSTATION_TRUST_ACCOUNT, ADS_UF_SERVER_TRUST_ACCOUNT;
- operatingSystem - Windows NT, Windows Server, Windows Workstation;
- operatingSystemVersion - Operating system version and build;
- operatingSystemServicePack - Operating system service pack numbe;r
- operatingSystemHotfix - List of hotfixes;
- whenCreated - When the object was created;
- whenChanged - When the object was modified.
Locating Computers Based on Computer Account Attributes:
Const ADS_SCOPE_SUBTREE = 2 Set objConnection = CreateObject("ADODB.Connection") Set objCommand = CreateObject("ADODB.Command") objConnection.Provider = "ADsDSOObject" objConnection.Open "Active Directory Provider" Set objCommand.ActiveConnection = objConnection objCommand.CommandText = _ "SELECT Name, Location, operatingSystemVersion FROM " _ & "'LDAP://DC=fabrikam,DC=com' WHERE objectClass='computer' " _ & "and operatingSystemVersion = '5.0 (2195)'" objCommand.Properties("Page Size") = 1000 objCommand.Properties("Timeout") = 30 objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE objCommand.Properties("Cache Results") = False Set objRecordSet = objCommand.Execute objRecordSet.MoveFirst Do Until objRecordSet.EOF Wscript.Echo "Computer Name: " & objRecordSet.Fields("Name").Value Wscript.Echo "Location: " & objRecordSet.Fields("Location").Value objRecordSet.MoveNext Loop
Trust Relation Ships
When you want to document your configuration, it is useful to have a list of all domain trust relation ships, especially during migrations, when you usually have a mix of trusts between Windows domains. Windows NT account domains, and Windows NT 4.0 resource domains. Each trust relationship in a domain has a trusted domain object that resides in the System container in the Domain naming context:
- flatName - The NetBIOS name of the domain for this trust;
- trustDirection - The direction of the established trust relationship: 0=disabled; 1=inbound; 2=outbound; 3=both (trusted and trusting);
- trustPartner - A string representing the DNS-style name of Windows domains or the NetBIOS name of down-level trust domains;
- trustType - The type of trust relationship established to the domain: 1=downlevel trust; 2=Windows trust; 3=MIT; 4=DCE.
Creating User Accounts
When creating a new user account, you must set the CN (unique in the account's OU) and Samaccountname (unique in the domain) attributes. The userPrincipalName attribute (unique in the enterprise) is optional. To create a new user in the OU:
Dim salesOU as IADsContainer Set salesOU = GetObject("LDAP://OU=Sales,DC=Fabrikam,DC=COM") Set usr = salesOU.Create("user", "CN=Jay Adams") usr.Put "sAMAccountName", "jayadams" usr.Put "userPrincipalName", "email@example.com" usr.Put "title", "Marketing Manager" usr.SetInfo usr.SetPassword "seahorse" usr.AccountDisabled = False usr.SetInfo
There are three scopes for groups:
- Domain Local.
Groups are of these types:
- Security - To control access to resources, and to use them as e-mail distribution lists;
- Distribution - Only used for e-mail distribution lists.
Group type is required. Specify an integer that contains the flags that specify the group type and scope using these combinations:
- 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;
- Domain local distribution - ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP;
- Universal Distribution - ADS_GROUP_TYPE_DOMAIN_UNIVERSAL_GROUP.
The process of creating a group is similar to other objects. First bind to the OU which will contain the group. Next create the group object. Then set the group type, and samAccountName for down-level clients.
Set ou = GetObject( "LDAP://OU=DSys,DC=Northwind,DC=tld" ) Set grp = ou.Create( "group", "CN=Distributed System Admin" ) ' Creating a domain local group grp.Put "groupType", ADS_GROUP_TYPE_LOCAL Or ADS_GROUP_TYPE_SECURITY_ENABLED grp.Put "samAccountName", "DSysAdmin" grp.SetInfo
Use the add method to add members to a group using the path to the user's object.
' Adding a user to a group grp.Add( "LDAP://CN=James Smith,OU=Marketing,OU=DSys,DC=Northwind,DC=tld" )
Managing Enterprise Configuration Information
Configuration information is global information shared among all domains in the enterprise and usually managed by the enterprise administrator.
The partitions container's crossRef objects list the enterprise naming contexts - one for Configuration, one for Schema and one for each Domain. You can add objects to point to partitions on other LDAP servers that are not part of the enterprise, in which case you generate an LDAP referral to the proper partition when requesting an object from that portion of the namespace.
This script enumerates crossRef objects in the partitions container:
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
Listing Domain Controllers
Each domain controller in the enterprise is represented by several objects:
- A computer object residing in the Domain renaming context (the computer account for the machine);
- a nTDSDSA object (NTDS settings) residing in the Configuration naming context. NTDS settings is a container that represents an instance of the directory service; it holds the DC's inbound replication connections. If you accidentally remove a NTDS Settings, the DC owning the object will resurrect the deleted object;
- A Servers object residing under the Serverscontainer object in the DC's site.
To get a list of all enterprise DCs, issue a query with a base path of the site's container that searches for object category nTDSDSA. Some attributes of interest:
- AdsPath - Use the full path to the object to find the parent object, which contains the DNS hostname of the DCs;
- HasMasterNCs - A multi-valued list of all the writable naming contexts. For this release of Windows, these are Schema, Configuration and Domain;
- Options - Bit 1 indicates if this DC is also GC.
Listing domain controllers:
' Get the Configuration Naming Context Set oRootDSE = GetObject("LDAP://RootDSE") strConfigNC = oRootDSE.Get("configurationNamingContext") ' Set up the oConnectionection set oConnection = CreateObject("ADODB.Connection") oConnection.Provider = "ADsDSOObject" oConnection.Open "ADs Provider" ' Build the query strQuery = "<LDAP://" & strConfigNC & ">;(objectClass=nTDSDSA);ADsPath;subtree" set oCmd = CreateObject("ADODB.Command") oCmd.ActiveConnection = oConnection oCmd.CommandText = strQuery Set oRecordset = oCmd.Execute ' Iterate through the results If oRecordset.Eof and oRecordSet.Bof Then WScript.Echo "No Domain Controllers were found" Else While Not oRecordset.EOF Set oParent = GetObject(GetObject(oRecordset.Fields("ADsPath")).Parent) ' Output the name of the server WScript.Echo "Server: " & oParent.cn & " dNSHostName: " & oParent.dNSHostName oRecordset.MoveNext Wend End if
Flexible Single-Master Operations (FSMO) implements operations (there are only a few) that must be handled in a single master replication model. There are five FSMO roles, two per enterprise and three per domain, and you can use scripting to find out which DCs hold which FSMO roles.
The Schema Master is unique in the entire enterprise; it alone can create new classes or attributes, after which it replicates updates to all domains in the forest. To identify it, read the value of fsmoRoleOwner attribute on the Schema container found under the Configuration container. Schema and Configuration naming contexts are enterprise wide and reside on all domain controllers.
Set objRootDse = GetObject( "LDAP://RootDse" ) Set objSchema = GetObject( "LDAP://" & objRootDse.get( "SchemaNamingContext" )) WScript.Echo objSchema.fsmoRoleOwner
Domain Naming Master
The Domain Naming Master is unique in the enterprise; it manages the addition and removal of domains into the forest. To identify it, read the partitions container object and examine its fsmoRoleOwner attribute:
Set objRootDse = GetObject( "LDAP://RootDse" ) Set objDomains = GetObject( "LDAP://CN=Partitions," & _ objRootDse.Get( "ConfigurationNamingContext" )) WScript.Echo objDomains.fsmoRoleOwner
The PDC Emulator is a per-domain role. To identify it, read the value of the fsmoRoleOwner attribute on the DomainDNS object:
Set objRootDse = GetObject( "LDAP://RootDse" ) Set objPDC = GetObject( "LDAP://" & objRootDse.Get( "DefaultNamingContext" )) WScript.Echo objDomains.fsmoRoleOwner
The RID Master is a per-domain role. It allocates RID blocks for all DCs in a domain that are used for SIDs in security principals such as users and groups. To identify it, read the value of the fsmoRoleOwner attribute on the RIDManager object of name CN="Rid Manager$" found in the domain's System Container:
Set objRootDse = GetObject( "LDAP://RootDse" ) Set objRID = GetObject( "LDAP://CN=Rid Manager$,CN=System," & _ objRootDse.Get( "DefaultNamingContext" )) WScript.Echo objDomains.fsmoRoleOwner
The Infrastructure Master is a per-domain role. To identify it, read the value of the fsmoRoleOwner attribute on the InfrastructureUpdate object for the domain:
Set objRootDse = GetObject( "LDAP://RootDse" ) Set objInfra = GetObject( "LDAP://CN=Infrastructure," & _ objRootDse.Get( "DefaultNamingContext" )) WScript.Echo objDomains.fsmoRoleOwner