NDS Application Development Using C++ (Part II)
Articles and Tips: article
Technical Support Engineer
Asia Pacific Support
01 Mar 1995
Part I of this DevNote (which appeared in the Jan/Feb issue of Novell Developer Notes) described a library of C++ classes that can be used as an interface to NetWare Directory Services. Part II introduces four additional base classes to handle server connections, container and volume auditing, and NDS object and attribute values.
Previous DevNotes in This Series Jan/Feb 95 NDS
Application Development using C++
- Introduction
- DSConn and DSConnBuffer Classes
- DSAudit Class
- DSFile Class
- Objects and Values
- Further Development and Notes
- Downloading the Class Library
Introduction
Part I of this article described a library of C++ classes for developing NetWare Directory Services (NDS)-aware applications. The benefit of the library is in the object-oriented design that provides automatic initialization and release of NDS resources with data encapsulation and data hiding. Hence code that would normally be required for error checking and recovery can be omitted since this is performed by class constructors and destructors.
The library achieves data encapsulation and data hiding by recording network handles, such as directory contexts, buffers and connection handles, as member fields. Member functions then have access to the handles, and hence the number of parameters required to access the NDS API functions is reduced.
The first part of this article introduced four classes: DSLocale, DSContext, DSBuffer and DSIteration. These control the initialization of Unicode and locale information, creation of a directory context, and the creation of buffer space to access NDS. The DSIteration class is a dummy class used to transfer data from the NDS to the application. Developers are responsible for deriving classes from DSIteration to process the data. An abridged example using these classes is given in Listing 1.
Listing 1
# define DSCPP_IOSTREAM # include "dslocale.h" # include "dsbuffer.h" # include "dsiterat.h" class DSListIteration : public DSIteration { // ... } ; int main(int argc, char * argv[]) { DSLocale dsLocale ; DSListIteration dsListIteration ; DSBuffer dsBuffer(argc == 2 ? argv[1] : "") ; char context[MAX_DN_BYTES] ; if (dsLocale.status() || dsBuffer.status()) return 0 ; dsBuffer.GetContext(context) ; cout << "Objects found under \"" << context << "\"\n" ;< dsBuffer.List("",& dsListIteration) ;& return 0 ; }
This article introduces four new base classes to handle server connections, container and volume auditing, and NDS object and attribute values. The DSConn class is used to establish a connection to a NetWare 3 or NetWare 4 server. The class can then be used to set trustee rights and map drives once the connection has been made. DSAudit is derived from DSConn and is used for container and volume auditing.
The container and volume auditing API have been combined into a single interface through the DSAudit class.
NDS objects and attribute values can be manipulated using the DSObject, DSValue and DSFile classes. DSObject provides a base class for defining NDS objects using DSValue and DSFile. Similarly DSValue provides a base class for defining NDS values. DSFile class allows NDS stream values, such as login scripts, to be read and modified (stream values cannot be accessed using the normal buffer API).
DSConn and DSConnBuffer Classes
To be able to access the resources of a file server, it is first necessary to establish a connection to the server, and depending on the resources required, identify yourself to the server. When a connection to a server is no longer required it is best to close the connection to release the resources used to maintain the connection.
The principal difference in connecting to a NetWare 3 server and connecting to a NetWare 4 server is in identifying the owner of the connection. With NetWare 3 the user identifies himself to the server, a process that is repeated for each connection. With NetWare 4 the user identifies himself to the NDS to generate a credential and signature.
Once the credential/signature pair has been generated the user can establish a connection without needing to identify himself explicitly (the identification is performed in the background). The credential/signature pair is generated by the NDS API function NWDSLogin().
The NWDSLogin() function has an equivalent member function in the DSContext class since the function does not presume on any connection nor require NDS buffer space. For the login function to work the preferred Directory Service tree has to be set. This is normally done in the client's NET.CFG file. If the preferred DS tree has not been defined then it can be obtained from the preferred server using the NDS function NWIsDSServer().
.When generating an object's credential/signature pair, the NWDSLogin() function only searches for the object in the context passed to the function. To perform the same search as the command line utility LOGIN.EXE it is necessary to search the context holding the nearest DS server. The context of the nearest DS server is obtained by getting the server's distinguished name and then removing server from the string.
The preceding procedure is implemented as the member function DSContextLoginToNDS() and is given in Listing 2. (Developers should note that the NWDSLogin() function requires a large amount of stack space, it is suggested that the application has a minimum of 8K).
Listing 2
NWDSCCODE DSContextLoginToNDS( NWPSTR object, NWPSTR password, NWDS_VALIDITY period) { char serverName[48] ; BYTE type = 0 ; NWCONN_HANDLE connHandle ; if (password == 0) password = "" ; if ((Status = NWGetPreferredConnName((BYTE*)serverName,& type)) != 0)& return Status ; if (type != NWNDS_CONNECTION){ char treeName[48] ; if (NWGetConnectionHandle((BYTE*)serverName,0,&connHandle,0) &&& NWAttachToFileServer ((char*)serverName,0,& connHandle))& return Status = 0x8847 ; if (NWIsDSServer(connHandle,treeName) == 0) return Status = 0x8847 ; if ((Status = NWSetPreferredDSTree(strlen(treeName),treeName))!= 0) return Status ; } if ((Status = NWGetPreferredDSServer(&connHandle)) != 0)& return Status ; if (Login(object,password,period)){ char serverBuffer[MAX_DN_BYTES] ; char objectBuffer[MAX_DN_BYTES] ; char * serverContext ; Status = NWGetNearestDirectoryService(& connHandle) ;& if (Status || GetServerDN(connHandle,serverName) || CanonicalizeName(serverName,serverBuffer)) return Status ; serverContext = NWLstrchr(serverBuffer,'.') ; sprintf(objectBuffer,".%s%s",object,serverContext) ; Login(objectBuffer,password,period) ; } return Status ; }
A NetWare 4 connection can be in one of three states, attached, authenticated and licensed. The attached state is the initial state after the connection handle has been obtained using the API function NWAttachToFileServer(). An attached connection can only view NDS data that has [Public] access.
Once the credential/signature pair has been generated a NetWare 4 connection can be authenticated. This identifies the object to the server and allows the object to access NDS data available to the object. When a connection has been authenticated it can then be licensed. A licensed connection can be used to access a server's resources. As the name suggests, a licensed connection uses one of the server's connection licenses, an authenticated connection does not.
The DSConn class has five constructors, one for NetWare 3 connections, two for NetWare 4 connections and two for duplicating an existing connection. The prototypes for the DSConn constructors are given in Listing 3.
Listing 3
DSConn(NWPSTR server, NWPSTR objectName = 0, NWPSTR password = 0, NWOBJ_TYPE objectType = OT_USER) ; DSConn(NWDSContextHandle context, NWPSTR server) ; DSConn(NWDSContextHandle context, NWCONN_HANDLE connHandle = 0) ; DSConn(NWCONN_HANDLE connHandle = 0) ; DSConn(DSConn &) ;
If the object name is omitted, then the user GUEST is substituted; if the password is omitted then a zero-length string is used. The constant DSCPP_NEAREST_NDS_SERVER can be used for the connHandle parameter to get a connection to the nearest NDS server.
Before a connection is made to a server, the constructor determines if a connection already exists. If a prior connection does exist, then the connection handle is copied and the object flags the connection so that it is preserved when the DSConn object is destroyed.
If no prior connection exists, then a new connection is established, which will be closed when the object is destroyed. The action of maintaining and closing connections when a DSConn object is destroyed can be modified using the preserve() and release() member functions.
Further connections to other servers can be made by using the member functions AttachToFileServer(), LoginToFileServer(), Authenticate() and LockConnection(). These functions will first close the current connection, unless the connection has been flagged for preservation.
After a connection has been established with a server, the application can then access the resources on the server. This normally involves mapping drive letters, transferring files and modifying trustee rights. The standard NWCALLS API requires a connection handle to specify the designated server. This can be obtained from a DSConn object automatically, using the conversion member function operator NWCONN_HANDLE().
Since mapping drives and modifying trustee rights are common operations, these functions have equivalent member functions in the DSConn class. The member functions for modifying trustee rights can use either NDS object names or server object identifiers. To translate between object names and object identifiers use the member functions MapNameToID() and MapIDToName().
Developers are warned that object identifiers are server-centric and any particular identifier should only be used during the current execution of the application. Listing 4 gives an example of translating object identifiers between servers. Such a scheme might be used when copying trustees from one server to another.
Listing 4
DSContext dsContext ; DSConn serverA(dsContext,"SERVERA") ; DSConn serverB(dsContext,"SERVERB") ; NWOBJ_ID objectA, objectB ; char objectName[MAX_DN_BYTES] ; // ... serverA.MapIDToName(objectA,objectName) ; serverB.MapNameToID(objectName,objectB) ;
The DSConnBuffer class is a combination of the DSBuffer and DSConn classes. The prototype for the constructor is identical to that of DSBuffer. The Context member field of the DSConn class is set to the DSContext base object in the DSBuffer class. The DSConnBuffer class would be used when an application required the services of a single server at a time.
Listing 5 gives an example of using the class to attach to the nearest DS server.
Listing 5
DSConnBuffer dsBuffer ; char szLoginName[MAX_DN_CHARS] ; char szPassword[32] ; // ... dsBuffer.LoginToNDS(szLoginName,szPassword) ; // ... dsBuffer.LoginToFileServer(DSCPP_NEAREST_NDS_SERVER) ;
DSAudit Class
Auditing was introduced with NetWare 4 so that major institutions can monitor events on the server and generate audit history files. The audit files can be only examined and modified by a designated auditor, who is normally a user external to the company. The auditor's account is first created by the system administrator who gives the auditor the user password and the audit password.
The auditor is then responsible for changing the audit password to making the audit system secure. While the intended use for auditing was for external auditing of company transactions, auditing is useful for general monitoring of the server. Possible uses include security breaches and network utilization.
The auditing API is divided into two groups, NDS container auditing and bindery volume auditing. The majority of the functions in the API have two versions, one for container auditing and one for volume auditing, with similar prototypes. The DSAudit class combines the two versions into one API set that can be used for both types of auditing.
The particular version of the function to be used depends on whether DSAudit object was opened for container or volume auditing using the member functions LoginAsContainerAuditor() or LoginAsVolumeAuditor().
Auditing is a server-centric operation requiring a connection handle to the server recording the audit information, and server object identifiers for the container/volume object being audited. Hence the DSAudit class is derived from the DSConn class so that the DSAudit class inherits the connection handle and object/identifier mapping functions from the DSConn class. The prototypes for the DSAudit constructors are given in Listing 6.
Listing 6
DSAudit(NWDSContextHandle context, NWPSTR server) : DSConn(context,server) DSAudit(NWDSContextHandle context, NWCONN_HANDLE conn = 0) : DSConn(context,conn)
Listing 7 is an example of reading a container audit file. Error checking has been removed from the example, in general this should be done by using the status() member function after each NDS function call. The connection handle to the server and the server object identifier of the container are obtained using the DSContext member function AuditGetObjectID().
Listing 7
NWCONN_HANDLE conn ; DWORD objectID ; char buffer[1024] ; char name[256], container[256] ; // ... dsContext.AuditGetObjectID(container,conn,objectID) ; DSAudit dsAudit(dsContext,conn) ; char * password = getpass("Audit password: ") ; if (dsAudit.LoginAsVolumeAuditor(objectID,(BYTE*)password)){ printf("Cannot login as auditor - 0x%X\n",dsAudit.status()) ; return 0 ; } dsAudit.InitAuditFileRead(0) ; WORD retSize = 0, count = 0 ; ftime * date ; AuditDSRecord * audit = (AuditDSRecord *) buffer ; while(dsAudit.ReadAuditingFileRecord(buffer,sizeof(buffer),retSize)==0) if (retSize > 0){> count ++ ; date = (ftime *) & audit-&dosDateTime ;& if (dsAudit.MapIDToName(audit->userID,name)) name[0] = 0 ;> printf("EventID %d\n",audit->eventTypeID) ;> printf("User ID %08lX %s\n",audit->userID,name) ;> printf("Time %02u/%02u/%02u %02u:%02u:%02u\n", date->ft_day, date->ft_month,date->ft_year,> date->ft_hour,date->ft_min, date->ft_tsec*2) ;> } printf("\n%d records read\n",count) ; dsAudit.LogoutAsAuditor() ;
The contents of the buffer returned from ReadAuditingFileRecord() depend on the event type identifier. Following the AuditDSRecord (for containers) or AuditRecord (for volumes) structure in the buffer is an EventUnion union which contains a structure definition for each event. The structure definitions can be found in the file NWAUDIT.H.
The current implementation of DSAudit is not compatible with the DOS utility AUDITCON. Passwords used by DSAudit API are not accepted by AUDITCON and vice versa. This anomaly is being investigated.
DSFile Class
The NDS has a fixed set of formats called syntaxes for recording attribute values. The structures and typedefs used to define these formats can be found in the file NWDSATTR.H. As described in the first article, attribute values are transferred between an application and the NDS using buffers.
The only format that cannot be transferred using buffers is the SYN_STREAM syntax, used to record files in NDS. The most common use for SYN_STREAM values are user and profile login scripts, but they can contain any binary data.
Your programs can access the files associated with SYN_STREAM values using the standard low level C file operations, read(), write(), lseek(), close(), etc. You specify the name of the file with an object name/attribute name pair and open it using the NDS function, NWDSOpenStream().
This function returns a file descriptor that can then be used directly with the standard C functions, or used to initialize a FILE or C++ stream object.
A few factors should be considered when using SYN_STREAM objects. The attribute holding the stream and the stream itself are not created when a stream value is opened for writing. Hence the attribute and a zero length file have to be created using the normal buffer commands.
When a stream is opened for writing the old contents are not deleted, but overwritten. Streams are also opened in binary mode only. Hence care should be taken when writing text strings to a stream object. Lastly an attribute can only have one stream value, multiple values are not allowed.
The DSFile class handles the first two points by automatically creating the attribute and value when the file is opened, and deleting the old contents when the stream is opened for write only. Listing 8 shows the procedure used to create new stream objects.
Listing 8
NWDSCCODE DSFilecreate(NWPSTR object, NWPSTR attr) { NWDS_BUFFER NWFAR * buffer ; if ((Status = NWDSAllocBuf(DEFAULT_MESSAGE_LEN,& buffer)) != 0)& return Status ; NWDSInitBuf (Context,DSV_MODIFY_ENTRY,buffer) ; NWDSPutChange (Context,buffer,DS_ADD_ATTRIBUTE,attr) ; NWDSPutChange (Context,buffer,DS_ADD_VALUE,attr) ; NWDSPutAttrVal(Context,buffer,SYN_STREAM,0) ; Status = NWDSModifyObject(Context,object,0,0,buffer) ; if (Status == ERR_ATTRIBUTE_ALREADY_EXISTS || Status == ERR_DUPLICATE_VALUE || Status == ERR_CANT_HAVE_MULTIPLE_VALUES) Status = 0 ; NWDSFreeBuf(buffer) ; return Status ; }
Listing 9 shows an example of using a DSFile object to modify a users login script. Note that the text added to the file uses \r\n for newline, since the file can only be opened in binary mode.
Listing 9
{ DSLocale dsLocale ; DSBuffer dsBuffer ; DSFile dsFile(dsBuffer) ; if (dsFile.open(username,"Login Script", DS_READ_STREAM|DS_WRITE_STREAM) == -1) return 0 ; printf("Login Script before change\n") ; PrintFile(dsFile) ; dsFile.lseek(0,SEEK_END) ; dsFile.write("REM New line at the end\r\n",25) ; dsFile.lseek(0,SEEK_SET) ; printf("Login Script after change\n") ; PrintFile(dsFile) ; return 0 ; } void PrintFile(int fd) { ifstream stream(fd) ; char buffer[512] ; while (stream.getline(buffer,sizeof(buffer))) puts(buffer) ; }
Objects and Values
The most complex part of developing an object-oriented approach to NDS programming is the handling of NDS objects and values. This is due to the number and variety of NDS syntaxes used to represent values and the open structure used to define NDS objects.
In this section the base classes DSValue and DSObject are introduced to provide a generic base for recording and modifying NDS objects. In the following text, references to the classes DSValue and DSObject are used to refer to classes derived from them.
The DSValue class contains one member field Syntax of type NWSYNTAX_ID, used to record the syntax of a value. Classes derived from DSValue are used to record the data. It is not necessary to define a class for each NDS syntax, since many of the syntaxes have similar storage requirements.
In this way the class DSVString handles all the single character string syntaxes and DSVInteger handles SYN_INTEGER and SYN_COUNTER. If the Syntax field is zero then the object is considered to be undefined.
The interaction between the DSObject and DSValue classes is implemented by the following DSValue member functions. The function assign(NWSYNTAX_ID,void*) is used to initialize the object from an NDS buffer. The DSValue object copies the data so that the NDS buffer can be reused.
The function data() returns a pointer to the object's data so that it can be copied to an NDS buffer. The function release() returns any dynamic memory used by the object and sets the Syntax field to zero. Finally, the function compare() is used to determine if two objects hold the same value.
NDS values recorded in DSValue objects are stored using equivalent data structures to those in the file NWDSATTR.H. The structures are not identical since templates are used in some cases and the field names have been changed, however the data is binary-compatible with NDS. DSValue objects can be modified either directly by modifying the data structure, or by using manipulators similar to those in IOMANIP.H.
Each field in the data structure has a corresponding manipulator to change the value, and where applicable the object's syntax. An example of using manipulators is given in Listing 10.
Listing 10
DSVInteger integer ; DSVInteger counter ; DSVString everyone ; DSVStringList languages ; DSVMultiValue multiValue ; integer << Integer(-1L) ;< counter << Counter( 1L) ;< everyone << String("Everyone.Container.Organization") ;< everyone << Syntax(SYN_DIST_NAME) ;< languages << String("C++") << String("C") << String("Pascal") ;< multiValue << Assign(integer) << Assign(counter) ;
In the first release of the library, only the classes DSVString, DSVStringList, DSVInteger, DSVBoolean and DSVMultiValue have been implemented. Volunteers to implement the remaining classes are welcome to contact the author, who will then send them the class definition with his appreciation.
As detailed in the first article, an NDS object consists of a collection of attributes where each attribute has a fixed syntax and can hold multiple values. To implement an NDS object in a C++ class, the derived DSObject provides two arrays to list the attributes the class is interested in and the number of values of each attribute that the class can hold.
These arrays are accessed using the pure virtual member functions AttributeNames() and AttributeCount(). The derived class also has a field derived from DSValue for all the values specified in the attribute names and attribute count arrays. The class' DSValue fields are accessed using the pure virtual member function DSValue * FindAttribute(int attrIndex, int valueIndex).
The first parameter is the index of the attribute in the attribute name array (starting at zero). The second parameter is the occurrence of the value for that attribute (starting at zero). The function should return a pointer to the DSValue object, or null if the attribute should not be used.
Rather than having to provide several DSValue objects to record a multiple value attribute, the class DSVMultiValue can be used. This class records an arbitrary number of DSValue objects in a linked list. All values must have the same syntax.
Listing 11 gives an example of defining a class to record the Surname, Full name, Security equals and Language attributes of NDS user objects.
Listing 11
NWPSTR dsUserAttrNames[] = {"Surname", "Full Name", "Security Equals", "Language", 0 } ; WORD dsUserAttrCount[] = { 1, 1, 1, 1, 0 } ; class DSUserObject : public DSObject { protected: DSValue * FindAttribute(WORD attrIndex, WORD valueIndex) ; NWPSTR * AttributeNames() { return dsUserAttrNames ; } WORD * AttributeCount() { return dsUserAttrCount ; } NWPSTR ClassName() { return "USER" ; } public: DSVString SurName ; DSVString FullName ; DSVMultiValue Security ; DSVStringList Language ; } ; DSValue * DSUserObjectFindAttribute(WORD attrIndex, WORD) { switch (attrIndex) { case 0: return & SurName ;& case 1: return & FullName ;& case 2: return & Security ;& case 3: return & Language ;& } return 0 ; }
The pure virtual member function ClassName() is used to specify the class name when an NDS object is created. The attribute name array must be terminated with a null pointer. In the preceding example only single attribute values are recorded for the Surname, Full name and Language attributes, hence only the last value read will be recorded. To record the first value simply test the valueIndex for zero. Multiple values are recorded for the Security equals attribute.
NDS objects are read from and written to NDS using the DSObject member functions readObject() and writeObject(). Both functions take a directory context and a relative distinguished name. The writeObject()function also takes an optional DSObject pointer. If the pointer is null, then a new object is created, otherwise the old object is modified. An example of using the DSUserObject defined in Listing 11 is given in Listing 12.
Listing 12
{ DSLocale dsLocale ; DSBuffer dsBuffer ; DSUserObject newUser, oldUser ; char fullname[MAX_DN_BYTES], distname[MAX_DN_BYTES], context [MAX_DN_BYTES], everyone[MAX_DN_BYTES] ; // ... sprintf(fullname,"%s %s",loginName,surName) ; sprintf(distname,".%s.%s",loginName,context) ; sprintf(everyone,"Everyone.%s",context) ; if (dsBuffer.DoesObjectExist(distname) == 0){ if (newUser.ReadObject(distname,& dsBuffer)){& cerr << "Cannot read user \"" << distname << "\"\n" ;< return 0 ; } oldUser = newUser ; cout << "\nOld user value\n" << oldUser ;< newUser.Language.release() ; newUser.Language << String("English") << String("C++")< << String("Pascal") << String("Fortran") ;< if (newUser.WriteObject(distname,& dsBuffer,& oldUser)){& cerr << "Cannot modify user \"" << distname << "\"\n" ;< return 0 ; } cout << "\nNew user value\n" << newUser ;< } else { newUser.SurName << String(surName) << Syntax(SYN_CI_STRING) ;< newUser.FullName << String(fullname) << Syntax(SYN_CI_STRING) ;< newUser.Security << String(everyone) << Syntax(SYN_DIST_NAME) ;< newUser.Language << String("English") ;< if (newUser.WriteObject(distname,& dsBuffer)){& cerr << "Cannot create user \"" << distname << "\"\n" ;< return 0 ; } dsBuffer.GenerateObjectKeyPair(distname,"") ; cout << "\nNew user value\n" << newUser ;< } return 0 ; }
Before a user object can be used to log into a server, the object's public/private key pair have to be generated (these keys are used to create the credential and signature). The key pair is generated in the preceding example immediately after the user object is created.
If the key pair are not generated then the command utility LOGIN.EXE will report that the user cannot be found in the correct context.
Further Development and Notes
Two classes are included in the library that have not been mentioned in this article. These are the DSSchema and DSPartition classes. Both classes are derived from the DSBuffer class and are useful for investigating the structure of the NDS tree.
An instructive exercise for developers wishing to extend the NDS schema is to use the DSSchema class to display the current NDS schema so that they can cross check their modifications.
The library has been implemented as a DOS static library and a Windows DLL using the Borland C++ 4 compiler. All class, function and function template declarations have the macros _DSCLASSDEC, _DSFUNCDEC and _DSTEMPDEC inserted into their declaration.
The compile time definition DSCPP_EXPORT should be set true when compiling the library into a DLL. This will export the class and function definitions. (For reasons unknown to the author, the function templates had to be declared inline when creating a DLL using Borland C++ 4.5, this generates a few warnings but no errors).
When using a DLL created from this library the compile time definition DSCPP_IMPORT should be set true. This will import the class the function definitions. When using static libraries then both DSCPP_EXPORT and DSCPP_IMPORT should be false. The source files have been organized to take advantage of the pre-compiled header option available on Borland compilers. Developers who use Borland C++ should pre-compile the header file DSDEFS.H.
The author would like any feedback from developers who have ported the library to other platforms. You can contact him via email at jbuckle@novell.com.
The prototypes used in the library are based on the 4.1 release of the Novell SDK. Developers who are using a previous version of the Novell SDK must set the compiler definition DSCPP_OLD_VERSION true so that dummy definitions of the new functions are included.
The examples listed in both articles are based on the NetWare 4.1 release of the schema. Some of these examples will not work with earlier schema since new attribute names have been introduced.
Downloading the Class Library
The NDS class library can be downloaded from Compuserve's NOVLIB forum, Library 11. It is in a self-extracting arvchive file named DN511.EXE.
* Originally published in Novell AppNotes
Disclaimer
The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.