Novell is now a part of Micro Focus

Developing NLMs Using User-Security Equivalences

Articles and Tips: article

RICH LEE
Senior Research Engineer
Novell Systems Research

KEVIN KINGDON
RSA Inc.

CARL TIETJEN
Software Engineer
Network Security Team

01 Feb 1996


In this article, the authors show how to develop NLMS using user-security equivalences to obtain server application specific services and how to use basic access control information associated with NetWare. This DevNote centers on how to represent access control inforamtion in a simple way within NetWare Directory Services (NDS). The associated code (available online) implements a discretionary security mechanism within NetWare, using operating system and application specific client/server controls to enforce security. This DevNote also shows the difference between "security equivalence" which is the calculated security attribute coming from security code in the operating system, and "discretionary access control," which is data in NDS.

Introduction

In this article, the authors show how to develop NLMS using user-security equivalences to obtain server application specific services and how to use basic access control information associated with NetWare. While this NLM code does not provide system level security, and relies entirely upon a secure OS architecture (which extends to the client) as provided in Novell Global Security Architecture, it does allow developers to program to NetWare security APIs. Topics developed in this article are a brief description of services, security equivalences, and access controls as they relate to the Client-Server paradigm.

This DevNote centers on how to represent access control information in a simple way within NetWare Directory Services (NDS). The associated code (available online) implements a discretionary security mechanism within NetWare. It utilizes operating system and application specific client/server controls to enforce security. It shows how to obtain the NCP connection security equivalence and use it with the access control attributes stored in NDS. This article shows the difference between "security equivalence" which is the calculated security attribute coming from security code in the operating system, and "discretionary access control," which is data in NDS.

It is important to note this difference because NDS access controls are not controls (the mechanism) but in fact the service attributes the operating system uses in exercising controls. For developers, this means the NDS database will be used by the operating system to gather some of the access control service attributes. The developer can then access the derived security control expression and use it in the client communication.

Here the security of the actual authentication mechanism is maintained by the operating system and-with the provision of a Class C2 evaluated implementation-provides a high degree of assurance that the mechanism works correctly and is tamperproof as evaluated at the Class C2 level. This means there are no "dirty hooks" allowing one to access the inputs or outcome of the security control mechanism. However, this article discusses a dirty hook that does allow programmers to utilize the mechanism based-on the established authentication of the user.

Importance of an Internal Security Mechanism

Developers often want to provide some level of security for applications. Often, the reasons for doing this are to enhance the perceived value of the application, as well as meet some customer criteria the developer is trying to satisfy.

While developers may find themselves in the precarious position of being both market and management driven to include the currently "wanted" security features, it is important to maintain a high-level perspective as to which features are "real" and which are bogus.

The principle of relinquished control is highly applicable to security penetrations. When either the operating system or the application code relinquishes control, malicious code has the opportunity to enter, examine, or extract vital information. If the operating systems are not secure (Class C2 evaluated and above), and the customer has not included Class C2 workstations as part of the security regimine in an evaluated configuration, there is little need to include application security "features"-because the system will not be secure. This perspective is based on the assumption that providing customers with "not real" security features is doing them a disservice because they then believe they are secure when in fact they are not.

It is important to recognize that real security originates with the operating system platform-both client and server (in a networked state), and must be independently evaluated if it is to be of any real value. This can be perilous territory for the applications/systems programmer, since application security developement is dependent on both an evaluated operating system and an application security mechanism.

Developers need to recognize that if they do not have independent evaluations done on their security mechanisms, someone else will-most probably in the trade press after the application or security implementation has been hacked.

Developing robust and realistic security features can be problematic for the developer. Since implementation of initial security features often occurs at a single workstation (client) which the developer is utilizing for development, considerations as to the broad applicability of the security mechanism may be in question when it is distributed to several machines working together. Design of security mechanism is heavily dependent on the process architecture under which it is implemented.

Any implementation of cryptography must be balanced against the threat to and risk of exposure to each individual client implementing the application. While some form of encryption might be used in a standalone secure workstation-in an effort to establish confidentiality, or to limit physical transportability of the data, this same security mechanism might be inappropriate for a more distributed system (network), and in fact could abet a potential attacker.

NetWare operates in a true client/server implementation (process architecture). There are security considerations in a network configuration that go beyond the simple hiding of information. It is no longer a case of simply providing the customer with perceived value or a check box, since any lack of "real" security may infact bring about the disclosure of sensitive information with progressively harsh penalties leading to the demise of the customers business or perhaps ones own business.

Clearly the development of security features now revolves around a new set of values,different from just the market potential for the inclusion of security features. Here it is of little value to provide NetWare developers with a "GetSecretKey" function for dealing with the operating system. The operating systems keystore would be of little value when anyone could pass the call to get the secret key from within any application. This makes it impractical for an application to log in and authenticate for the user.

What then is the point to having an authentication secret in the first place? If the rule of "relinquished control" prevails, then only a secure authentication mechanism linked directly to the boot of the workstation would assure that identification secrets could not be easily captured. Subsequent attempts to authenticate in operation after the operating system has relinquished control would only server to complicate, and perhaps expose the authentication secret. Subsequent attempts to establish alternate application security services, outside of the initial authentication, all operate after control is relinquished, and are the ready target of malicious software. Consequently, one solution is to provide security equivalence access, without doing any of the following:

  • asking the user to re-enter the password

  • providing the "secret key," or its equivalent

Asking the user to re-enter the password at anytime after the initial boot is just plain foolhardy. It is subject to serveral types of attack, the least of which will gain knowledge of the password.

If you do not mind having mock protections, this is an excellent way to provide a viable hack to anyone interested in gaining part of the authentication secret. Secondly, can anyone put the password in and gain access to an already authenticated session. Again, not a good idea.

It is just not a good policy to request user passwords a second time! Where security concerns are key, developers should recomend customers use an evaluated operating system security equivalence (like Class C2) as an authentication mechanism to establish identity. Doing this enables applications on the network in the following ways:

  • It enables SSO, the Single Sign-On, a direction which NetWare has taken.

  • It protects authentication secrets from malicious code.

Most developers recognize how sensitive authentication information is in a distributed environment (see workstation architecture). It is important for the operating system not to relinquish control prior to initial authentication (see NetWare Security Architecture), and it is important to not release authentication secrets after the operating system has relinquished control. This article shows application developers-who want security enabled applications ability-just how to get it-without violating network security.

The Features

The sample code makes use of three security features in NetWare:

  • SSO

  • Identification and Authentication

  • Security Equivalences

As mentioned earlier, with SSO the person does not have to remember two passwords or where the application resides. So you look up an object in the directory. The object has the host address, and the connection to the server. Identification and authentication are implicit to the process as is the resolution of security equivalence.

What is not apparent is how this is done. We use a dirty hook in CLIB. It exists, and it is a security equivalent to access control-control decisions (not to the access controls themselves) -that you write in the applicaton. This allows application security to have similar behvavior to NDS and the file system.

Caveat. Without the techniques in this program you cannot implement the same security that NetWare uses for access controls and security equivalence. Taking an alternate course can be risky since changes in NetWare could leave the application out of sync.

A Look at the Code

The source code used in this Developer Note can be found in the NOVUSER forum of Compuserve.

SUGGEST.H

The first sample code we will look at is SUGGEST.H, the header file for inclusion in the Sinstall.c sample code file.

In SUGGEST.Hthe following items are of note:

Line 15: the class and the attribute that are going to extend the schemas are defined and prototyped. The SINSTALL.c code goes in and extends the NDS schema. SUGGEST.H creates instances of the attributes, one for the definition of class and one for definition of the attribute.

ADDOBJ.C code will create the actual object. It instanciates the objects which are the values of the three things we are doing in the "Suggestion Box" code. It is the list of who can operate, who can list, and who can read-the suggestions placed in the suggestion box.

In this example, we use WATCOM version 10.0 and Novell 4.1 SDK: The resultant parameter sets are dependent on the makefiles in the MAKEINIT.FILE which is in the last part of the sample code.

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability, title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

**

*/

#define SUGGESTIONS  "Suggestions""
#define SUGGESTION_SUBMITTERS   "Suggestion Submitters""
#define SUGGESTION_READERS      "Suggestion Readers""
#define SUGGESTION_OPERATORS    "Suggestion Operators""
#define SUGGESTION_BOX_CLASS    "Suggestion Box""
#define NOVELL                  "CN=NOVELL""
#define SMALL_BUFFER_LEN  512

int Add StreamAttribute(NWDSContextHandle context, char *serverName, cgar *inFile);

int AddOurACLAttributes(NWDSContextHandle context, char *userName);

int AddOurACLValue(NWDSConextHandle context, NWSYNTAX_ID syntax ID, Char *objName, 

                  char *attrName, void *attrVal);

int CreateServerObject(NWDSContextHandle context, char *serverName);

int DefineAttributes(NWDSContextHandle context);

int DefineClass(NWDSContext Handle context);

NCPEXT.C

This is the code for the NCP extension; it has to have the NCP extension and the name of the extension box. This is the resident set of the NCP extensions and they have to work with a specific object. You need the name of the specific object to make the NCP extensions work; in the case of our example, the name is "Suggestion Box," and line 134 is where the registration is done.

There is an undocumented call in line 91. Earlier in line 36 is a function prototype to this undocumented call. This call is not listed in any documentation. As mentioned, line 36 is the prototype for the call, which works this way:

Initially, you give the station connection number (of someone who is logged in) and the ID of the user (same someone).

The first is the client connection number, and the second is the ID. Then, the data you get back is what you want to check against. The code uses an equivalence compare and does all of the equivalence checks for you. If I am logged in at this station, everyone I am equivalent to is available and the function compares equivalence to the second ID. It gives all of the security equivalences.

This is an awesome API, and is it is not in the SDK.


Note:You can use the functionprototype here, but must use the makefileorit will not work. See Line 64 for the externalcommand.

External Command. The External Command allows late binding, at load time. If you want to link up to the NetWare Security in the operating system, this is the way to do it.

If you are using the 4.1 version of the SDK that shipped after the initial 4 release, this may already be covered. Otherwise you will not see a reference to this, so it may not be of interest. Still, the basic idea here is that of logging in to the NDS server to use the schema; however it is necessary to have the server logged in because you cannot log in directly to root.

It is important to understand that we are not making changes to NDS; we just need access to read the object. We have the possibility to change things in the suggestion box, and enter suggestions. This is "main" and we are seeking login to get access. Also, you will have to make sure you are in the right thread group, see the SDK-you set a speicfic thread and then in line 10 you allocate the buffer.

The next part is run from the client, which you can find in Client.C, but for the moment we will continue with the commentary on NCPEXT.C.

Three different NCP registrations are located at lines 114, 124, and 134. In this code, there is a separate NCP for each list. While you could have them all as a single NCP, this example shows them implemented separately. Line 114 registers the NCP extension so that when someone sends an NCP, you handle it for them and register the calllback procedure. Line 145 sets the current context.

Suggestion Submitter/Suggestion Reader. Starting with line 97 is the suggestion subbmitter. This code processes whenever someone calls the NCP; you have to give the function's name to the call; then the NCP does the NDS work for you. Since there is a fair amount of NDS set up, this code calls and reads the object getting values from it.

In line 332 is one of the interesteing undocumented calls. You pass it the client connection number, and it returns the equivalence object ID.

This is not just a compare but also a compare for equivalence. If I am in a group and the group is in this list, the group has equivalence; I then have to iterate through the object.

"Reset" suggestion box clears the suggestions. The whole set is stored as a NDS object. It is one of the file types.

Submit suggestions. Submitting suggestions is where the the real work is done.

Looking at line 225, thread manipulations occur, and in line 230 the buffer is initiated. These are followed by Line237 where you find client connection number and convert it to a useful format.

In Line 255 you get the username and find if there is a match in the list. Then put the infomation from 141 into the attribute name. In Line 255 do a compare. So in Line260 to 263 if the right person is there, it just runs. If the right person is not there, some interesing codes starts in line 265.

From Line 260, 270 you can see the init of the buffer set and read from the DS object. This is done to see what is out there. Then the code takes the object ID and maps it to a name.

By Line 320, you can see what the local name is and what it is equivalent to. Here, we find which item in the list the client name is equivalent to. This will find out if the person has the right to do anything.

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability,title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

**

*/

#include <ncpext.h<<
#include <stdio.h<<
#include <string.h<<
#include <advanced.h<<
#include <process.h<<
#include <conio.h<<
#include <nwmisc.h<<
#include <errno.h<<
#include <nwsemaph.h<<
#include <nwdsapi.h<<
#include <io.h<<
#include <nwconn.h<<
#include <nwenvrn.h<<
#include <nwcntask.h<<
#define  SUGGESTIONS            "Suggestions""
#define  SUGGESTION_SUBMITTERS  "Suggestion Submitters""
#define  SUGGESTION_READERS     "Suggestion Readers""
#define  SUGGESTION_OPERATORS   "Suggestion Operators""
// Function Prototypes

LONG IsEquivalent(LONG station,LONG ID);

BYTE SubmitSuggestion(NCPExtensionClient *client, void *requestData,

         LONG requestDataLen, void *replyData, LONG *replyDataLen);

BYTE ReadSuggestion(NCPExtensionClient *client, void *requestData,

         LONG requestDataLen, char *replyData, LONG *replyDataLen);

BYTE ResetSuggestion(NCPExtensionClient *client, void *requestData,

         LONG requestDataLen, void *replyData, LONG *replyDataLen);

// Global Variables

int  myThreadGroupID;

long semaphoreHandle;

int  streamFileHandle;

NWDSContextHandle context;

NWDS_BUFFER *nameBuf,*objInfo;

WORD myConnNum;

char serverName[48];

char NCP1ExtName[] = "SUGGESTION SUBMITTER";"
char NCP2ExtName[] = "SUGGESTION READER";"
char NCP3ExtName[] = "SUGGESTION RESET";"
struct queryDataStruct

{

LONG unused[8];

}*q1,*q2,*q3;

/*--------------------------------------------------------------

*/

void main(int argc, char *argv[])

{

int err;

LONG rc;

if (argc != 2)

 {

   printf("NCPEXT "name of Suggestion Box" \r\n");"
   exit(1);

  }

strcpy(serverName,argv[1]);

if ((context = NWDSCreateContext()) == ERR_CONTEXT_CREATION)

{  

  printf("Error %d in NWDSCreateContext.\n", (int)context);  "
  exit(1);  

}  

if (err = NWDSLoginAsServer(context))

  goto Cleanup0;

myThreadGroupID = GetThreadGroupID();

if (SetThreadContextSpecifier(GetThreadID(), NO_CONTEXT) != ESUCCESS)

{

 ConsolePrintf("Error doing pre"registration procesing (Set Context)\n");"
 goto Cleanup0;

}

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &nameBuf)) & 0)&
{

 ConsolePrintf("Error doing pre"registration procesing (Alloc nameBuf)\n");"
 goto Cleanup0;     

}     

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &objInfo)) & 0)&
{

 ConsolePrintf("Error doing pre"registration procesing (Alloc objInfo)\n");"
 goto Cleanup1;     

}     

if (err = NWRegisterNCPExtension(NCP1ExtName, SubmitSuggestion, NULL, NULL, 1, 0, 0, &q1))&
{

 ConsolePrintf("Error %d registering NCP Extension: %s\n", err, NCP1ExtName);"
 goto Cleanup2;

}

ConsolePrintf("Registered NCP Extension: %s\n", NCP1ExtName);"
if (err = NWRegisterNCPExtension(NCP2ExtName, ReadSuggestion, NULL, NULL, 1, 0, 0, &q2))&
{

 ConsolePrintf("Error %d registering NCP Extension: %s\n", err, NCP2ExtName);"
 goto Cleanup2;

}

ConsolePrintf("Registered NCP Extension: %s\n", NCP2ExtName);"
if (err = NWRegisterNCPExtension(NCP3ExtName, ResetSuggestion, NULL, NULL, 1, 0, 0, &q3))&
{

 ConsolePrintf("Error %d registering NCP Extension: %s\n", err, NCP3ExtName);"
 goto Cleanup2;

} 

ConsolePrintf("Registered NCP Extension: %s\n", NCP3ExtName);"
if ((err = NWDSOpenStream(context, serverName, SUGGESTIONS, DS_READ_STREAM | DS_WRITE_STREAM,

     &streamFileHandle)) & 0)&
{

 printf("Could not open file \r\n");"
 goto Cleanup2;

}

if ((rc = SetCurrentConnection( 1)) == EFAILURE) 
{

  printf("could not get a new connection");"
  goto Cleanup2;

}

myConnNum = GetConnectionNumber();

semaphoreHandle = OpenLocalSemaphore(1);

printf("Press any key to unregister NCPs.\n");"
getch();

if (err = NWDeRegisterNCPExtension(q1))

 ConsolePrintf("Error %d Deregistering NCP Extension: %s\n", NCP1ExtName);"
else

ConsolePrintf("NCP Extension %s Deregistered\n", NCP1ExtName);"
if (err = NWDeRegisterNCPExtension(q2))

 ConsolePrintf("Error %d Deregistering NCP Extension: %s\n", NCP2ExtName);"
else

ConsolePrintf("NCP Extension %s Deregistered\n", NCP2ExtName);"
if (err = NWDeRegisterNCPExtension(q3))

 ConsolePrintf("Error %d Deregistering NCP Extension: %s\n", NCP3ExtName);"
else

ConsolePrintf("NCP Extension %s Deregistered\n", NCP3ExtName);"
if (err = CloseLocalSemaphore(semaphoreHandle))

  ConsolePrintf("Error closing semaphore \n");"
close(streamFileHandle);

Cleanup2:

 NWDSFreeBuf(objInfo);

Cleanup1:

 NWDSFreeBuf(nameBuf);

Cleanup0:

 NWDSFreeContext(context);

}

BYTE SubmitSuggestion(NCPExtensionClient *client, void *requestData,

    LONG requestDataLen, void *replyData, LONG *replyDataLen)

{

int savedThreadGroupID,err;

char objName[MAX_DN_CHARS*2 + 1];

char attrName[MAX_DN_CHARS*2 + 1];

char attrVal[MAX_DN_CHARS*2 + 1];

WORD connNumber, objType;

long objID;

BYTE loginTime[7];

NWFLAGS matched,verified=0;

LONG rc, position =0;

NWDS_ITERATION iteration;

NWCOUNT i,attrValCount,attrCount;

NWSYNTAX_ID syntaxID;

uint32 equivObjID; 

NWDSCCODE ccode;

if (requestDataLen < 1)<
{

 ConsolePrintf("%s reports bad parameters on call from client at ""
 "connection: %ld, task: %ld\n", client""connection, client""task);"
 return 0x5A;

}

savedThreadGroupID = GetThreadGroupID();

if (SetThreadGroupID(myThreadGroupID) == EFAILURE)

{

 ConsolePrintf("Could not set thread group");"
 return 0x5B;

}

if ((err = NWDSInitBuf(context, DSV_COMPARE, nameBuf)) < 0)<
{     

 ConsolePrintf("Could not Init nameBuf\n");     "
 ccode = 0x5C;     

 goto Cleanup1;

}     

connNumber = (WORD)client  connection; 
GetConnectionInformation(connNumber, &objName, &objType, &objID, loginTime);&
if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_SUBMITTERS)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSPutAttrVal(context, nameBuf, SYN_DIST_NAME, objName)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrVal\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSCompare(context, serverName, nameBuf, &matched)) & 0)&
{

  ConsolePrintf("Error %d from NWDSCompare\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

verified = matched;

if (!verified)

{

  if ((err = NWDSInitBuf(context, DSV_READ, nameBuf)) < 0)<
  {     

    ConsolePrintf("Could not Init nameBuf\n");     "
    ccode = 0x5C;     

    goto Cleanup1;

  }     

  if ((err = NWDSInitBuf(context, DSV_READ, objInfo)) < 0)<
  {     

   ConsolePrintf("Could not Init objInfo\n");     "
   ccode = 0x5C;     

   goto Cleanup1;

  }     

   

  if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_SUBMITTERS)) < 0)<
  {

   ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
   ccode = 0x5D;

   goto Cleanup1;

  }

  iteration = NO_MORE_ITERATIONS;

  do

  {

   if ((ccode = NWDSRead(context, serverName, DS_ATTRIBUTE_VALUES, FALSE, nameBuf, &iteration, objInfo)) & 0)&
   {

    ConsolePrintf("Error %d from NWDSRead\n",(int)ccode);     "
    ccode = 0x5D;

    goto Cleanup1;

   }

   if ((ccode = NWDSGetAttrCount(context, objInfo, &attrCount)) & 0)&
   {

     ConsolePrintf("Error %d from NWDSGetAttrCount\n",(int)ccode);     "
     ccode = 0x5D;

     goto Cleanup1;

   }

   if ((ccode = NWDSGetAttrName(context, objInfo, attrName, &attrValCount, &syntaxID)) & 0)&
   {

     ConsolePrintf("Error %d from NWDSGetAttrName\n",(int)ccode);     "
     ccode = 0x5D;

     goto Cleanup1;

   }

   for (i=0; i < attrValCount; i++)<
   {

    if ((ccode = NWDSGetAttrVal(context, objInfo, SYN_DIST_NAME, attrVal)) < 0)<
    {

      ConsolePrintf("Error %d from NWDSGetAttrVal\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    if ((ccode = NWDSMapNameToID(context, myConnNum, attrVal, &equivObjID)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSMapNameToID\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    rc = IsEquivalent(client  connection,equivObjID); 
    if (rc)

    {

      verified = TRUE;

      break;

    }

   }

  } while (iteration != NO_MORE_ITERATIONS);

}

if (verified)

{

 WaitOnLocalSemaphore(semaphoreHandle);

 position = lseek(streamFileHandle,0,SEEK_END);

 if ((write(streamFileHandle, requestData, requestDataLen)) == EFAILURE)

 {

  ConsolePrintf("Error writing suggestion to file\n");"
  ccode = 0x5F;

 }

 else

  ccode = 0;

  SignalLocalSemaphore(semaphoreHandle);

}

else

{

 ConsolePrintf("Unauthorized connection attemped to submit suggestion \n");"
 ccode = 0x5E;

}

Cleanup1:

if (SetThreadGroupID(savedThreadGroupID) == EFAILURE)

  ConsolePrintf("%s reports error restoring thread group context\n", NCP1ExtName);"
*replyDataLen = 0;

replyData = NULL;

return ccode;

}

BYTE ReadSuggestion(NCPExtensionClient *client, void *requestData,

    LONG requestDataLen, char *replyData, LONG *replyDataLen)

{

int savedThreadGroupID,err;

char objName[MAX_DN_CHARS*2 + 1];

char attrName[MAX_DN_CHARS*2 + 1];

char attrVal[MAX_DN_CHARS*2 + 1];

WORD connNumber, objType;

long objID;

BYTE loginTime[7];

NWFLAGS matched,verified=0;

LONG rc, fileSize;

NWDS_ITERATION iteration;

NWCOUNT i,attrValCount,attrCount;

NWSYNTAX_ID syntaxID;

uint32 equivObjID; 

NWDSCCODE ccode;

if (*replyDataLen < 1)<
{

 ConsolePrintf("%s reports bad parameters on call from client at ""
  "connection: %ld, task: %ld\n", client""connection, client""task);"
 return 0x5A;

}

savedThreadGroupID = GetThreadGroupID();

if (SetThreadGroupID(myThreadGroupID) == EFAILURE)

{

 ConsolePrintf("Could not set thread group");"
 return 0x5B;

}

if ((err = NWDSInitBuf(context, DSV_COMPARE, nameBuf)) < 0)<
{     

 ConsolePrintf("Could not Init nameBuf\n");     "
 ccode = 0x5C;     

 goto Cleanup1;

}     

connNumber = (WORD)client  connection; 
GetConnectionInformation(connNumber, &objName, &objType, &objID, loginTime);&
if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_READERS)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSPutAttrVal(context,nameBuf, SYN_DIST_NAME, objName)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrVal\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSCompare(context, serverName, nameBuf, &matched)) & 0)&
{

  ConsolePrintf("Error %d from NWDSCompare\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

verified = matched;

if (!verified)

{

 if ((err = NWDSInitBuf(context, DSV_READ, nameBuf)) < 0)<
 {     

   ConsolePrintf("Could not Init nameBuf\n");     "
   ccode = 0x5C;     

   goto Cleanup1;

 }     

 if ((err = NWDSInitBuf(context, DSV_READ, objInfo)) < 0)<
 {     

    ConsolePrintf("Could not Init objInfo\n");     "
    ccode = 0x5C;     

    goto Cleanup1;

 }

 if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_READERS)) < 0)<
 {

    ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
    ccode = 0x5D;

    goto Cleanup1;

 }

 iteration = NO_MORE_ITERATIONS;

 do

 {

    if ((ccode = NWDSRead(context, serverName, DS_ATTRIBUTE_VALUES,

         FALSE, nameBuf, &iteration, objInfo)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSRead\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    if ((ccode = NWDSGetAttrCount(context, objInfo, &attrCount)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSGetAttrCount\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    if ((ccode = NWDSGetAttrName(context, objInfo, attrName, 

        &attrValCount, &syntaxID)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSGetAttrName\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    for (i=0; i < attrValCount; i++)<
    {

      if ((ccode = NWDSGetAttrVal(context, objInfo, 

        SYN_DIST_NAME, attrVal)) < 0)<
      {

        ConsolePrintf("Error %d from NWDSGetAttrVal\n",(int)ccode);     "
        ccode = 0x5D;

        goto Cleanup1;

      }

      if ((ccode = NWDSMapNameToID(context, myConnNum, 

         attrVal, &equivObjID)) & 0)&
      {

        ConsolePrintf("Error %d from NWDSMapNameToID\n",(int)ccode);     "
        ccode = 0x5D;

        goto Cleanup1;

      }

      rc = IsEquivalent(client  connection,equivObjID); 
      if (rc)

      {

        verified = TRUE;

        break;

      }

    }

 } while (iteration != NO_MORE_ITERATIONS);

}

if (verified)

{

  WaitOnLocalSemaphore(semaphoreHandle);

  lseek(streamFileHandle,0,SEEK_END);

  fileSize = tell(streamFileHandle);

  lseek(streamFileHandle,0,SEEK_SET);

  if (fileSize < *replyDataLen) <
     *replyDataLen = fileSize;

  else

    *replyDataLen = *replyDataLen 1; 
  rc = *replyDataLen;

  if ((read(streamFileHandle, replyData, *replyDataLen)) == EFAILURE)

  {

    ConsolePrintf("Error reading suggestion file\n");"
    ccode = 0x5F;

  }

  else

   ccode = 0;

  replyData[*replyDataLen] = '\0';

  *replyDataLen = *replyDataLen+1;

  SignalLocalSemaphore(semaphoreHandle);

}

else

{

 ConsolePrintf("Unauthorized connection attemped to read suggestions \n");"
 ccode = 0x5E;

}

Cleanup1:

 if (SetThreadGroupID(savedThreadGroupID) == EFAILURE)

   ConsolePrintf("%s reports error restoring thread group context\n",NCP2ExtName);"
 return ccode;

}

BYTE ResetSuggestion(NCPExtensionClient *client, void *requestData, LONG requestDataLen, 

void *replyData, LONG *replyDataLen)

{

int savedThreadGroupID,err;

char objName[MAX_DN_CHARS*2 + 1];

char attrName[MAX_DN_CHARS*2 + 1];

char attrVal[MAX_DN_CHARS*2 + 1];

WORD connNumber, objType;

long objID;

BYTE loginTime[7];

NWFLAGS matched,verified=0;

LONG rc;

NWDS_ITERATION iteration;

NWCOUNT i,attrValCount,attrCount;

NWSYNTAX_ID syntaxID;

uint32 equivObjID; 

NWDSCCODE ccode;

savedThreadGroupID = GetThreadGroupID();

if (SetThreadGroupID(myThreadGroupID) == EFAILURE)

{

 ConsolePrintf("Could not set thread group");"
 return 0x5B;

}

if ((err = NWDSInitBuf(context, DSV_COMPARE, nameBuf)) < 0)<
{     

 ConsolePrintf("Could not Init nameBuf\n");     "
 ccode = 0x5C;     

 goto Cleanup1;

}     

connNumber = (WORD)client  connection; 
GetConnectionInformation(connNumber, &objName, &objType, &objID, loginTime);&
if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_OPERATORS)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSPutAttrVal(context, nameBuf, SYN_DIST_NAME, objName)) < 0)<
{

  ConsolePrintf("Error %d from NWDSPutAttrVal\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

if ((ccode = NWDSCompare(context, serverName, nameBuf, &matched)) & 0)&
{

  ConsolePrintf("Error %d from NWDSCompare\n",(int)ccode);     "
  ccode = 0x5D;

  goto Cleanup1;

}

verified = matched;

if (!verified)

{

  if ((err = NWDSInitBuf(context, DSV_READ, nameBuf)) < 0)<
  {     

    ConsolePrintf("Could not Init nameBuf\n");     "
    ccode = 0x5C;     

    goto Cleanup1;

  }     

  if ((err = NWDSInitBuf(context, DSV_READ, objInfo)) < 0)<
  {     

    ConsolePrintf("Could not Init objInfo\n");     "
    ccode = 0x5C;     

    goto Cleanup1;

  }

  if ((ccode = NWDSPutAttrName(context, nameBuf, SUGGESTION_OPERATORS)) < 0)<
  {

    ConsolePrintf("Error %d from NWDSPutAttrName\n",(int)ccode);     "
    ccode = 0x5D;

    goto Cleanup1;

  }

  iteration = NO_MORE_ITERATIONS;

  do

  {

    if ((ccode = NWDSRead(context, serverName, DS_ATTRIBUTE_VALUES,

        FALSE, nameBuf, &iteration, objInfo)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSRead\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    if ((ccode = NWDSGetAttrCount(context, objInfo, &attrCount)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSGetAttrCount\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    if ((ccode = NWDSGetAttrName(context, objInfo, attrName, 

       &attrValCount, &syntaxID)) & 0)&
    {

      ConsolePrintf("Error %d from NWDSGetAttrName\n",(int)ccode);     "
      ccode = 0x5D;

      goto Cleanup1;

    }

    for (i=0; i < attrValCount; i++)<
    {

      if ((ccode = NWDSGetAttrVal(context, objInfo, SYN_DIST_NAME, attrVal)) < 0)<
      {

        ConsolePrintf("Error %d from NWDSGetAttrVal\n",(int)ccode);     "
        ccode = 0x5D;

        goto Cleanup1;

      }

      if ((ccode = NWDSMapNameToID(context,myConnNum, attrVal, &equivObjID)) & 0)&
      {

        ConsolePrintf("Error %d from NWDSMapNameToID\n",(int)ccode);     "
        ccode = 0x5D;

        goto Cleanup1;

      }

      rc = IsEquivalent(client  connection,equivObjID); 
      if (rc)

      {

        verified = TRUE;

        break;

      }

    }

  } while (iteration != NO_MORE_ITERATIONS);

}

if (verified)

{

   WaitOnLocalSemaphore(semaphoreHandle);

   lseek(streamFileHandle,0,SEEK_SET);

   if ((write(streamFileHandle, NULL, 0)) == EFAILURE)

   {

    ConsolePrintf("Error reseting suggestion file\n");     "
    ccode = 0x5F;

   }

   else

    ccode =0;

  SignalLocalSemaphore(semaphoreHandle);

}

else

{

  ConsolePrintf("Unauthorized connection attemped to reset suggestion file \n");"
  ccode = 0x5E;

}

Cleanup1:

 if (SetThreadGroupID(savedThreadGroupID) == EFAILURE)

    ConsolePrintf("%s reports error restoring thread group context\n",NCP3ExtName);"
 *replyDataLen = 0;

 replyData = NULL;

 return ccode;

}

SINSTALL.C

Line 29 declares the password function. Line 60 is "main, " and line 62 gets a string and declares a variable to take the login name and another for the password length. The array is 81 characters maximum length.

Line 79 must log in to Directory Services (DS) to change the schema-at ROOT, the top of the tree. Here the NLM must log in to NDS. So the NLM will be run from the console, and will log into DS. It has to have sufficient rights, so it has to Be assigned rights as an admin of the root in order to be run at the console. Line 87 NWDSLogin is there; just get in. There are not enough rights to get it from the server; the server does not have the rights change NDS. Line 88 does login password or goes for "CLEANUP"

Line 89 puts your object name in as the parameter and finds out WHOAMI. Line 93: If err, it goes off to define the class and the object and adds the access control list attributes. This application runs once. It gets and defines the new items from SUGGEST.H. The DS is changed and the create is done to instanciate NDS. In the newly created new object class are some mandatory attributes. The object has mandatory and some optional attributes. Now the code must go out and instanciate the object, creating values for those optional attributes.

The values are added four times-creating the class , creating the object, adding the attributes to that object, and providing the values. We know what the three attributes for our object are. For example, into the "submitters" attribute are put those that are authorized to submit. We also put in the DS root name so anyone can submit. In "readers" and "operators" we are adding self.

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability, title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

**

*/

#define NWNLM

#include <nwdsapi.h<<
#include <stdio.h<<
#include <stdlib.h<<
#include <conio.h<<
#include "suggest.h""
#define ENTER_KEY  0x0d

#define ESCAPE_KEY  0x1b

#define BACKSPACE_KEY 0x08

#define DELETE_KEY  0x53

#define PWD_LEN   81

/*-----------------------------------------------------------------------

*/

GetPassword(char *Password)

{

int c;

int i;

printf("Enter password: ");"
*Password=i=c=0;

do

{

 if((c=getch()) == BACKSPACE_KEY)

 {

   if(i)

      Password[  i] = 0; 
   else

      RingTheBell();

 }

 else if(c == ENTER_KEY);

 else if(c == DELETE_KEY || c == ESCAPE_KEY)

   Password[i=0] = 0;

 else if(i < PWD_LEN<2)<
 {

   Password[i++] = c;

   Password[i] = 0;

 }

 else

   RingTheBell();

} while(c != ENTER_KEY);

printf("\n");"
}

void main(int argc, char *argv[])

{

char loginName[MAX_DN_CHARS + 1], password[PWD_LEN];

int err;

NWDSContextHandle context;

char myObjectName[256];

if (argc != 2)

{

  printf("sInstall "name of Suggestion Box" \r\n");"
  exit(1);

}

if ((context = NWDSCreateContext()) == ERR_CONTEXT_CREATION)

{

  printf("Error %d in NWDSCreateContext.\n", (int)context);"
  goto Cleanup0;

}

printf("Enter a login name: ");"
gets(loginName);

printf("\n");"
GetPassword(password);

printf("\n");"
if (err = NWDSLogin(context, 0, loginName, password, 0))

 goto Cleanup0;

if (err = NWDSWhoAmI(context, &myObjectName))&
 goto Cleanup0;

if ((err = DefineAttributes(context)) || (err = DefineClass(context))

 || (err = CreateServerObject(context, argv[1]))

 || (err = AddOurACLAttributes(context, argv[1]))

 || (err = AddOurACLValue(context, SYN_DIST_NAME, argv[1], SUGGESTION_SUBMITTERS, DS_ROOT_NAME))

 || (err = AddOurACLValue(context, SYN_DIST_NAME, argv[1], SUGGESTION_SUBMITTERS, &myObjectName))&
 || (err = AddOurACLValue(context, SYN_DIST_NAME, argv[1], SUGGESTION_READERS, &myObjectName))&
 || (err = AddOurACLValue(context, SYN_DIST_NAME, argv[1], SUGGESTION_OPERATORS, &myObjectName)))&
{

 printf("Error %ld in installing Suggestion Box.\n", err);"
 goto Cleanup1;

}

Cleanup1:

NWDSLogout(context);

Cleanup0:

NWDSFreeContext(context);

}

ADDVAL.C

ADDVAL.C is done to add access control list (ACL) attributes to the object-the list of who can submit, read, and administrate the SUGGESTION BOX. In ADDVAL.C with the object and the mandatory attributes created, the optional attributes still have to be done. Lines 30 to 35 handle buffer allocation and put needed values in and use NWDSModifyObject to modify the object

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability, title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

**

*/

#include <nwdsapi.h<<
#include <stdio.h<<
#include <stdlib.h<<
#include <string.h<<
#include <io.h<<
#include <fcntl.h<<
#include "suggest.h""
/*----------------------------------------------------------------------

*/

int AddOurACLAttributes(NWDSContextHandle

context, char *objName)

{

int err;

Buf_T *buffer;

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &buffer)) & 0)&
  return (err);

if (((err = NWDSInitBuf(context, DSV_MODIFY_ENTRY, buffer)) < 0)<
    || ((err = NWDSPutChange(context, buffer, DS_ADD_ATTRIBUTE, SUGGESTION_SUBMITTERS)) < 0)<
    || ((err = NWDSPutChange(context, buffer, DS_ADD_ATTRIBUTE, SUGGESTION_READERS)) < 0)<
    || ((err = NWDSPutChange(context, buffer, DS_ADD_ATTRIBUTE, SUGGESTION_OPERATORS)) < 0))<
  goto Cleanup0;

if (((err = NWDSModifyObject(context, objName, 0, 0, buffer)) < 0)<
   && err != ERR_ENTRY_ALREADY_EXISTS)&
  printf("Error: %d modifying object\n", err);"
else

  printf("Attributes successfully added to object %s.\n",objName);"
Cleanup0:

NWDSFreeBuf(buffer);

return err;

}

int AddOurACLValue(NWDSContextHandle context, NWSYNTAX_ID syntaxID,

                   char *objName, char *attrName, void *attrVal)

{

int err;

Buf_T *buffer;

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN,&buffer)) & 0)&
  return (err);

if (((err = NWDSInitBuf(context, DSV_MODIFY_ENTRY, buffer)) < 0)<
    || ((err = NWDSPutChange(context, buffer, DS_ADD_VALUE, attrName)) < 0))<
  goto Cleanup0;

if ((err = NWDSPutAttrVal(context, buffer, syntaxID, attrVal)) < 0)<
  goto Cleanup0;

if (((err = NWDSModifyObject(context, objName, 0, 0, buffer)) < 0)<
  && err != ERR_ENTRY_ALREADY_EXISTS)&
  printf("Error: %d modifying object\n", err);"
else

  printf("Object %s successfully modified.\n",objName);"
Cleanup0:

NWDSFreeBuf(buffer);

return err;

}

DEFINE.C

DEFINE.C S is part of SINSTALL.C. Line 20-25 are lists of the attributes needed. C_top is the superclass and can contain this object common name-what the object is called and what you need to get it running.The part from line 45 down is sample code. There is a loop to define the attributes.

The cool thing is that the code is resuable, and line 45 can be plunked into other code and reused. All you have to do is change 35 to 40 with your own attributes. You would have to do something similar to lines 24 and 25 and for 70 thru 100.

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability, title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

** 

*/

#define NWNLM

#include <nwdsapi.h<<
#include <stdio.h<<
#include <stdlib.h<<
#include <string.h<<
#include "suggest.h""
char *superclasses[] =  {C_TOP, 0};

char *containment[] = {C_ORGANIZATION, C_ORGANIZATIONAL_UNIT, 0};

char *naming[] = {A_COMMON_NAME, 0};

char *mandatory[] = {A_COMMON_NAME, A_HOST_SERVER, 0};

char *optional[] = {A_DESCRIPTION, SUGGESTION_SUBMITTERS, SUGGESTION_READERS,

                   SUGGESTION_OPERATORS, SUGGESTIONS, 0};

char **itemNameList[] = {superclasses, containment,naming, mandatory, optional};

static struct

{

char *name;

int syntax;

} attrList[] = {

{SUGGESTION_SUBMITTERS, SYN_DIST_NAME},

{SUGGESTION_READERS, SYN_DIST_NAME},

{SUGGESTION_OPERATORS, SYN_DIST_NAME},

{SUGGESTIONS, SYN_STREAM}

};

/*--------------------------------------------------------------------------

*/

int DefineAttributes(NWDSContextHandle context)

{

int err, i;

Attr_Info_T attrDef;

attrDef.attrFlags = 0;

attrDef.attrLower = 0;

attrDef.attrUpper =  1; 
attrDef.asn1ID.length = MAX_ASN1_NAME;;

memset(attrDef.asn1ID.data, 0, MAX_ASN1_NAME);

for (i = 0; i < sizeof(attrList)/sizeof(attrList[0]); i++)<
{

  attrDef.attrSyntaxID = attrList[i].syntax;

  if (((err = NWDSDefineAttr(context, attrList[i].name, &attrDef)) & 0)&
      && err != ERR_ATTRIBUTE_ALREADY_EXISTS)&
    return err;

}

return 0;

}

/*--------------------------------------------------------------------------

*/

int DefineClass(NWDSContextHandle context)

{

int err, i, j;

Class_Info_T classData;

Buf_T *classItems;

classData.classFlags = DS_EFFECTIVE_CLASS;

classData.asn1ID.length = MAX_ASN1_NAME;

memset(classData.asn1ID.data, 0, MAX_ASN1_NAME);

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &classItems)) & 0)&
  return (err);

if ((err = NWDSInitBuf(context, DSV_DEFINE_CLASS, classItems)) < 0)<
  goto Cleanup0;

for (i = 0; i < sizeof(itemNameList)/sizeof(itemNameList[0]); i++)<
{

  if ((err = NWDSBeginClassItem(context, classItems)) < 0)<
  goto Cleanup0;

  for (j = 0; itemNameList[i][j]; j++)

  {

    if ((err = NWDSPutClassName(context, classItems, itemNameList[i][j])) < 0)<
    goto Cleanup0;

  }

}

if (((err = NWDSDefineClass(context, SUGGESTION_BOX_CLASS, &classData, classItems)) & 0)&
    && err != ERR_CLASS_ALREADY_EXISTS)&
  printf("Error: %d unable to define Suggestion Box class\n", err);"
else

  printf("Suggestion Box class is defined.\n");"
Cleanup0:

NWDSFreeBuf(classItems);

return (err == ERR_CLASS_ALREADY_EXISTS ? 0 : err);

}

ADDOBJ.C

This code could be done from the client. It creates an ojbect. Earlier we were changing the schema, and here are going to create the actual object. This is not as reuseable, but this is how you do it.

/*

** This software is provided as is and carries no warranty

** whatsoever.  Novell disclaims and excludes any and all implied

** warranties of merchantability, title and fitness for a particular

** purpose.  Novell does not warrant that the software will satisfy

** your requirements or that the software is without defect or error

** or that operation of the software will be uninterrupted.  You are

** using the software at your risk.  The software is not a product

** of Novell, Inc. or any of subsidiaries.

**

*/

#define NWNLM

#include <nwdsapi.h<<
#include <nwconn.h<<
#include <nwenvrn.h<<
#include <nwipxspx.h<<
#include <stdio.h<<
#include <stdlib.h<<
#include "suggest.h""
static struct

{

char *name;

int syntax;

} attrList[] = 

{

{A_OBJECT_CLASS, SYN_CLASS_NAME},

{A_HOST_SERVER, SYN_DIST_NAME},

{SUGGESTIONS, SYN_STREAM},

{A_DESCRIPTION, SYN_CI_STRING}

};

/*--------------------------------------------------------------------------

*/

int CreateServerObject(NWDSContextHandle context, char *serverName)

{

char localServer[48], description[] = "A Suggestion Box";"
int err, i;

void *attrValues[sizeof(attrList)/sizeof(attrList[0])];

uint32 status = 0;

Buf_T *buffer;

GetFileServerName(0, localServer);

if ((err = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &buffer)) & 0)&
   return err;

if ((err = NWDSInitBuf(context, DSV_ADD_ENTRY, buffer)) < 0)<
   goto Cleanup0;

attrValues[0] = SUGGESTION_BOX_CLASS;

attrValues[1] = localServer;

attrValues[2] = 0;

attrValues[3] = &status;&
attrValues[4] = description;

for (i = 0; i < sizeof(attrList)/sizeof(attrList[0]); i++)<
{

  if (((err = NWDSPutAttrName(context, buffer, attrList[i].name)) < 0)<
    || ((err = NWDSPutAttrVal(context, buffer, attrList[i].syntax, attrValues[i])) < 0))<
  goto Cleanup0;

}

if (((err = NWDSAddObject(context, serverName, 0, 0, buffer)) < 0)<
  && err != ERR_ENTRY_ALREADY_EXISTS)&
   printf("Error: %ld adding object to directory\n", err);"
else

   printf("Suggestion Box object successfully added to the Directory.\n");"
Cleanup0:

  NWDSFreeBuf(buffer);

  return (err == ERR_ENTRY_ALREADY_EXISTS ? 0 : err);

}

* 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.

© Copyright Micro Focus or one of its affiliates