Active Directory Service Interfaces - Advanced Topic

General

- GetInfoEx
- Filtering
- Property Caching
- GetEx

WinNT

- WinNT Binding
- Communicating to a specific domain controller
- Including backup domain controllers as part of selection

Active Directory

- Searching the entire tree
- Object Class vs Object Category
- Fast Bind
- Operational Attributes
- Specifying Page Size
- GUID searching
- Listing Mandatory and Optional Attributes
- Active Directory Security

GetInfoEx

When the first Get issue on an object, ADSI ,implicitly,brings all attributes which have 1) value 2) user has a read permission into ADSI cache. This is designed for optimizing network traffic, server load for most cases and ease of use- one big round trip is normally better than chatty traffics. However, in some cases, you like to get only a small set of attributes. GetInfoEx does exactly this. It allows you to specify only attributes you're interested in.
Example,
Set o = GetObject("LDAP://CN=Jsmith, DC=ArcadiaBay, DC=COM")
o.GetInfoEx Array("sn","givenName","telephoneNumber"), 0
Debug.Print o.Get("sn")
Debug.Pritn o.Get("givenName")
Back to top

Filtering

Often time you would like to enumerate only certain types of class in a container. It's more efficient to filter first, instead of doing comparision during the enumeration.
Example, instead of doing this
'--- Print all users in a domain
Set cont = GetObject("WinNT://ARCADIABAY")
For each obj in cont
if obj.Class = "User" then
Debug.Print obj.Name
End if
Next
Instead, you should do this:

'--- Print all users in a domain
Set cont = GetObject("WinNT://ARCADIABAY")
con.Filter = Array("user")
For each usr in cont
Debug.Print usr.Name
Next

Back to top
GetEx

If you want to read a property but don't know whether it is single-valued or multi-valued, GetEx comes in handy. Regardless how many values in an attribute, you can rely on GetEx for value enumeration.
Example,
Set usr = GetObject("LDAP://CN=Jsmith, DC=Fabrikam, DC=COM")
phone = usr.GetEx("telephoneNumber") 'single-valued property
For Each pnumber In phone
Debug.Print pnumber
Next
phone = usr.GetEx("otherTelephone") 'multi-valued property
For Each pnumber In phone
Debug.Print pnumber
Next

Back to top

Property Caching

With IADsPropertyList, IADsPropertyEntry and IADsPropertyValue, you will be able to manipulate ADSI caches.
Example,
Set oObject = GetObject("LDAP://CN=Andrew Anderson,OU=Sales,DC=Antipodes,DC=com")
WScript.Echo "User: " & oObject.cn
oObject.GetInfo 'Load into ADSI's caches
WScript.Echo "Property Count: " & oObject.PropertyCount
For i = 0 to oObject.PropertyCount -1
Set X = oObject.Item(i+1)
For Each Y In X.Values
WScript.Echo "Attribute: " & X.Name & " " & " (" & CStr(aProperty(Y.ADsType+1)) & ")"
Select Case Y.ADsType + 1
Case ADSTYPE_DN_STRING
WScript.Echo "Value: " & Y.DNString
Case ADSTYPE_CASE_EXACT_STRING
WScript.Echo "Value: " & Y.CaseExactString
Case ADSTYPE_CASE_IGNORE_STRING
WScript.Echo "Value: " & Y.CaseIgnoreString
Case ADSTYPE_PRINTABLE_STRING
WScript.Echo "Value: " & Y.PrintableString
Case ADSTYPE_NUMERIC_STRING
WScript.Echo "Value: " & Y.NumericString
Case ADSTYPE_BOOLEAN
WScript.Echo "Value: " & Y.Boolean
Case ADSTYPE_INTEGER
WScript.Echo "Value: " & Y.Integer
//... and more here....
Next
Next
Back to top

WinNT: Binding

In WinNT, you will be able to specify the object's class as part of the ADsPath. This will speed up the bind in most cases. Examples: "WinNT://ARCADIABAY/jsmith,user" , "WinNT://ARCADIABAY/marketing,group"

Back to top

WinNT: Communicating to a specific domain controller

If you specify bind to a domain or objects in a domain, ADSI, by default, communicates to Primary Domain Controller. In ADSI, there is a way, to select to a specific domain controller. Specify the computer name with ',computer' as part of ADsPath.
Example: "WinNT://bdc02,computer"

Note: if the domain controller is a backup domain controller, all the write operations will fail.

Back to top

WinNT: Including backup domain controllers as part of selection

ADSI introduces a new flag (ADS_READONLY_SERVER) for including back up domain controllers as part of selection criteria when bind to a domain. ADSI may select either PDC or BDC.
Example,
Set dso = GetObject("WinNT:")
Set obj = dso.OpenDSObject("WinNT://ARCADIABAY/jsmith,user", "Administrator", "secret", ADS_READONLY_SERVER )
Note: if the domain controller is a backup domain controller, all the write operations will fail.

Back to top

Active Directory: Searching the entire domain tree

Although it's very possible, chasing referral is discouraged for a domain tree search. The client may have to connect n number of domain controllers before getting the complete result set. Global catalog is designed for enterprise, tree search. Since not all attributes are replicated to Global Catalog, you may want to rebind to the targeted object once you retreive its distinguished name.

example,
Set gc = GetObject("GC://DC=Central, DC=ArcadiaBay, DC=COM")
//... do ADO search here...

Back to top

Active Directory: Object Class vs Object Category

Object class is a multi-valued attribute. Active Directory does not index this attribute for many reasons. Active Directory introduces a similar concept - objectCategory, is an indexed, single value attributes. Include objectCategory to search a certain type of class.
Note: With the exception of (objectClass=*), you should always include objectCategory for efficient searches.

Example,
-Find all users
(&(objectCategory=Person)(objectClass=user))

-Find all groups
(objectCategory=Group)
-Find all computers
(objectCategory=Computer)
-Find all object
(objectClass=*)

Back to top

LDAP: Fast Bind

When you request an LDAP bind using ADSI, one of things ADSI will do is to do a quick base search on that object itself. This is done for the following reasons: 1) to make sure the object exists 2) to get the object class so that ADSI will be able to load the appropriate ADSI extensions and interfaces. By specifying ADS_FAST_BIND, ADSI will skip this base search. This means that ADSI does not check if object's existence, and ADSI only supports IADs interface for this bound object.
One scenario is updating attributes on objects found in a result set. Since the objects are included in the result set, they are highly likely exist in the directory.
example,
Set dso = GetObject("LDAP:")
... do Search....
while rs.EOF
adsPath = rs.Fields("ADsPath")
Set obj = dso.OpenDSObject(adsPath, vbNullString, vbNullString, ADS_SECURE_AUTHENTICATION | ADS_FAST_BIND )
rs.MoveNext
wend
Back to top

Active Directory: Operational Attributes

Active Directory defines several operational attributes. An operational attribute is an artifact attribute. When one sets an operational attribute value, it, normally, triggers some actions on the server. On the other hand, when one gets an operational attribute value, the value itself, usually, is calculated on the server. Like a normal attribute, an operational attribute can be either multivalue or single value. The operational attribute may be in read-only, write-only or read and write mode.

Active Directory defines several operational attributes,for examples: canonicalName, allowedChildClassesEffective, allowedAttributes, possibleInferirors, lowestUncommitedUSN, and many more.

To retrieve or set an operational attribute value, you must use IDirectoryObject, or IADs::GetInfoEx
'--- Retrieve a canonical name (every object in Active Directory has a canonicalName)
Set obj = GetObject("LDAP://CN=Cert Publishers,CN=Users,DC=Arcadiabay,DC=com")
obj.GetInfoEx Array("canonicalName"), 0
Debug.Print obj.Get("canonicalName") '---it should be: arcadiabay.com/Users/Cert Publishers

//----- Find all allowed attributes for a given object in the current user security context -------

IIDirectoryObject *pObject;
hr = ADsGetObject(L"LDAP://DC=activeds,DC=ntdev,DC=Microsoft,DC=Com",
IID_IDirectoryObject, (void**) &pObject );
if (!SUCCEEDED(hr) )
{
return hr;
}
LPWSTR szAttrs[] = { L"allowedAttributesEffective" };
PADS_ATTR_INFO pAttrEntry;
DWORD dwReturn;
hr = pObject->GetObjectAttributes( szAttrs, 1, &pAttrEntry, &dwReturn );
if ( !SUCCEEDED(hr) )
{
pObject->Release();
return hr;
}
if ( pAttrEntry )
{
for( DWORD idx=0; idx < pAttrEntry->dwNumValues; idx++ )
{
printf("%ws\n", pAttrEntry->pADsValues[idx].CaseIgnoreString );
}
FreeADsMem( pAttrEntry );
}
pObject->Release();
return hr;

Back to top

Active Directory: GUID searching

Every object in Active Directory carries GUID which guarantees to be unique. GUID never changes even the object is renamed or moved. ADSI support GUID binding in the form of LDAP://<GUID=xxxxxx> or GC://<GUID=xxx>
For example of GUID binding please see Active Directory GUID Binding

Back to top

Active Directory: Listing Mandatory and Optional Attributes

Both mandatory and operational attributes can be obtained from IADsClass. To get the IADsClass interface, first you must know the class name of an object. Secondly, compose the ADSPath from the class name and schema path. Third, bind this object and ask for IADsClass. The MandatoryProperties and OptionalProperties attributes contain in this interface.

Dim x As IADsClass
Dim classNams As String
Dim provider As String

className = "user"
provider = "LDAP://schema/"
Set x = GetObject( provider & className )

For Each attr In x.MandatoryProperties
Debug.Print attr
Next

For Each attr In x.OptionalProperties
Debug.Print attr
Next

Back to top

Active Directory: Specifying Page Size

On subtree search, it's recommended to specify the page size. Even if you're not expecting to find many objects with the search. Paging allows the search to timeout in a re-startable way, so that you can resume the search where it left off. Without paging, the search will simply fail when it hits a time limit. Paging also promote scalability for both clients and servers.
ADSI handles the page size transparently. No need additional programming required, except specifying the page size in the search preferences.

VB Example,
Command.Properties("Page Size") = 500
'--Execute and enumerate ...
VC Example,
ADS_SEARCHPREF_INFO prefInfo[ MAX_SEARCH_PREF ];
prefInfo[dwCountPref].dwSearchPref = ADS_SEARCHPREF_PAGESIZE;
prefInfo[dwCountPref].vValue.dwType = ADSTYPE_INTEGER;
prefInfo[dwCountPref].vValue.Integer = m_nPageSize;
dwCountPref++;
hr = m_pSearch->SetSearchPreference( prefInfo, dwCountPref )

Back to top

Active Directory: Security

Active Directory Security covers in detail in the Active Directory Programmer's Guide.
Two common scenarios are presented here. - Organizational Unit Delegation and Setting ACL on an attribute
Example 1 - Organizational Unit Delegation

'---- Allow JSmith to create and delete users in the sales organization ----------
Dim ace As New AccessControlEntry
Dim secDesc As IADsSecurityDescriptor
Dim acl As IADsAccessControlList
Set x = GetObject("LDAP://OU=Sales,DC=Arcadiay,DC=com")
Set secDesc = x.Get("ntSecurityDescriptor")
Set acl = secDesc.DiscretionaryAcl
ace.ObjectType = "{BF967ABA-0DE6-11D0-A285-00AA003049E2}" '--- This is the User Class' schemaID GUID
ace.AccessMask = ADS_RIGHT_DS_CREATE_CHILD Or ADS_RIGHT_DS_DELETE_CHILD
ace.AceType = ADS_ACETYPE_ACCESS_ALLOWED_OBJECT
ace.AceFlags = ADS_ACEFLAG_INHERIT_ACE
ace.Flags = ADS_FLAG_OBJECT_TYPE_PRESENT
ace.Trustee = "ARCADIABAY\JSmith"
acl.AddAce ace
secDesc.DiscretionaryAcl = acl
x.Put "ntSecurityDescriptor", Array(secDesc)
x.SetInfo
Example 2 - Setting ACL on an attribute
‘-- Deny JSmith to write “Managed By” attributes
Dim ace As New AccessControlEntry
Dim secDesc As IADsSecurityDescriptor
Dim acl As IADsAccessControlList
Set x = GetObject("LDAP://OU=Sales,DC=Arcadiay,DC=com")
Set secDesc = x.Get("ntSecurityDescriptor")
Set acl = secDesc.DiscretionaryAcl
ace.ObjectType = "{0296C120-40DA-11D1-A9C0-0000F80367C1}” ‘Managed By
ace.AccessMask = ADS_RIGHT_DS_WRITE_PROP
ace.AceType = ADS_ACETYPE_ACCESS_DENIED_OBJECT
ace.AceFlags = ADS_ACEFLAG_INHERIT_ACE
ace.Flags = ADS_FLAG_OBJECT_TYPE_PRESENT
ace.Trustee = "ARCADIABAY\JSmith"
acl.AddAce ace
secDesc.DiscretionaryAcl = acl
x.Put "ntSecurityDescriptor", Array(secDesc)
x.SetInfo

Back to top