How to Access NDS from HTML or ASP, Part 4
Articles and Tips: article
01 Jul 2001
This AppNote, the fourth in a series, explains how to verify remote users against NDS with the help of ASP. For Part 3 of this series, see http://support.novell.com/techcenter/articles/ana20010506.html.
Verifying Remote Users Against NDS
Imagine that we want to create an Intranet ASP application that manipulates company data. Each employee of this company should have access to this application and to the data, but there should be various access levels implemented differentiating among employees. The company has already implemented an NDS tree solution for accessing network and network resources. So naturally we want our ASP application to authenticate users against the already existing DS database of users.
When a user starts the HTML browser and connects to the Web server, the following login dialog appears, asking for tree name, full distinguished name, and a valid password:
The login dialog box.
After the user enters the required information and presses the Verify button, the input data are sent to the ASP server, which verifies whether the user exists and whether he has NDS access. This part of our application is done by utilizing our NWDir control. The user is then informed of verification results. This is in short what our sample does. As for the last step -the code can be easily extended, according to the result. Valid users for example will be transferred to the appropriate page, but an unauthorized attempt will be refused and an alarm notification sent to the system administrator.
Here is the output generated by our sample code where the user was successfully verified:
OK, the task is clear so how do we code it? From the task description it seems that we should create two ASP files -one with input elements and buttons and the other processing verification and displaying the result. Yes, it would work this way, but this task is one where we can use a clever ASP technique -posting information to the originating ASP page. What does that mean exactly and why would we use it?
In a default HTML document, where any input elements are used, another (different) HTML document must also be defined, in which the input values from the first document will be passed, and where these inputs will be used in the logic, in computing, etc. The schematic data flow looks like this, simply from HTML1 to HTML2:
Contrary to this default HTML input data flow, ASP allows the input data to be (re)sent to the same originating document! This can save one or even more physical code pages and helps you keep order in required application files:
During the first load/run on the Web server, the ASP engine creates from the original ASP page the HTML1 document with several input elements. This document is then sent to the IE browser. In the HTML1 document, parameter ACTION= in <FORM> tag specifies the same original ASP document as a destination for entered data. So the same ASP page is reloaded and run on the Web server again, but this second run results in the creation of HTML2 document, which is then sent to the browser as a reply to a previously posted request.
Here is the code. There is nothing important during the starting lines:
<%@ LANGUAGE="VBSCRIPT" %> <!-- FILE: verify.asp --> <HTML> <HEAD> <TITLE> Verify user ASP sample code </TITLE> </HEAD> <BODY BGCOLOR=#66CDAA> <p> <Font face="Arial" size="+1" color="Red"> Verify user ASP sample code </Font> <BR><BR><BR>
Here is where it becomes tricky:
<% If IsEmpty(Request.Form("objectName")) Then Response.Write "Please enter following data:" %> <FORM ACTION="verify.asp" METHOD="POST"> <TABLE> <TR> <TD ALIGN="right">DS tree name:</TD> <td><INPUT TYPE="Text" NAME="treeName"></td> </TR> <TR> <TD ALIGN="right">Object FD name:</TD> <td><INPUT TYPE="Text" NAME="objectName"></td> </TR> <TR> <TD ALIGN="right">Password:</td> <TD><INPUT TYPE="Password" NAME="password"></td> </TR> <TR> <TD></TD> <TD> <INPUT TYPE="Submit" NAME="LoginButton" VALUE="Verify"> <INPUT TYPE="reset"> </TD> </TR> </TABLE> </FORM>
As you can see, we check whether the input field objectName is empty or not with the following code line:
If IsEmpty(Request.Form("objectName")) Then
This check is done on the server side (notice <%%> brackets), so it does not appear in the final HTML document at all. An empty input field means we are running this page for the first time. We must generate, from the original ASP document, an HTML code for the login screen with input elements.
In case the input field objectName is not empty, it undoubtedly means that the user has entered login data and we should process them against our NDS database:
<% Else 'User verification code goes here Dim retCode, entry Dim gotTreeName, gotObjectName, gotPassword, ndsName Set NWDir1=server.createobject("NWDirLib.NWDirCtrl.1") gotTreeName=Request.Form("treeName") gotObjectName=Request.Form("objectName") gotPassword=Request.Form("password") On Error Resume Next NWDir1.FullName="NDS:\\" & gotTreeName & "\[Root]" ndsName= NWDir1.FullNameFromTreeAndContext(gotTreeName, gotObjectName) response.write "Verifying password for object:<p> " response.write "<FONT COLOR=blue>" & ndsName & "</FONT><p>" " set entry=NWDir1.FindEntry(ndsName) If entry is nothing Then response.write "Error " & str(err.number) & ", " & err.description & "<br>" response.write "Object not found !" & "<BR>" Else retCode=entry.ValidatePassword(gotPassword) If retCode=TRUE Then response.write("Password is valid !<BR>") Else response.write("Password is NOT valid !<BR>") End If End If Set NWDir1=Nothing End If %> </BODY> </HTML>
In the first step of this processing we instantiate NWDir ActiveX control. Next, using the Request.Form() property we retrieve user data entered during the initial page load.
Then we continue initializing our NWDir1.Fullname with the name of the DS tree entered.
NWEntry class of NWDir offers one method for password verification - NWEntry.ValidatePassword(). The biggest advantage of the underlying DLL function, which is used in this method, is the fact that it does not require the calling process to be authenticated to the DS tree in order to return valid results. This saves our work, we do not care and do not need to login first in order to issue this call.
Prior to calling ValidatePassword() method we must set the focus of our NWDir1 on the object (entry) we want to verify. From the entered data we must first construct the fully distinguished object name:
and then set our focus on the given entry using NWDir.FindEntry() method:
If NWDir.FindEntry() method fails, it shows that the object was not found and we have to refuse verification accordingly.
In case the object was found and our focus was set, we can issue
to check if the entered password is valid or not.
Finally, before leaving, we are doing some cleanup:
With this sample code, keep in mind that it works properly only if the [Public] trustee assignment on the attached DS tree is set to default, which means Browse rights for [Public] throughout the whole DS tree. This is how rights are originally set when the DS is installed. If this condition is not met NWDir1.FindEntry() can fail, not being able to "see" the required entry because the ASP application is not authenticated to the DS tree and has only [Public] rights.
Login and Search DS using ASP
The following example demonstrates how ASP can be used with VBScript to process a login into DS tree and then to search for a given object in the DS tree. So basically we have two separate tasks here to achieve - login and search.
Unfortunately, the NWDir ActiveX component does not have any login or authentication functionality implemented. It simply supposes that the user (or application) that utilizes it is already logged in to one or more DS trees. So for a login purpose in this sample code we have to use NWSess control. Fortunately there is no problem using this control. The NWSess control can be easily embedded and used in both HTML and ASP pages.
The story behind the DS search in NWDir control is a bit more complicated. NWDir control offers NWDir.Search() method, which is intended to search for an object of a given name and type. The designers of this control supposed (quite rightfully) that the search through the DS tree may take a long time, especially when the tree is a huge one and specified search parameters require searching the whole tree. So the idea is not to block the application during the search and pass output search results via fired event after the NWDir.Search() method has finished. In reality you can specify in NWDir.Search() whether you want search DS database synchronously or asynchronously, whether your application will be blocked by the search or not.
This is a good idea, and the good news is that it works well in all standard environments. The bad news is that the search results are always passed back to the application only via fired event, no matter whether you search synchronously (in blocking mode) or asynchronously (in nonblocking mode). Another bit of bad news is that the ASP engine simply does not support events generated by embedded objects! ASP knows and supports only its own events, session, and application OnStart() and OnEnd() events, which can be specified and used in the global.asa file.
What can we do with the search from our ASP application then? Fortunately there are two possible solutions to get out of this trap:
Using a wrapping technique to capture events in ASP code
Using another control for the search - NWDirQuery
Let's discuss both of them.
Using a Wrapping Technique
The idea is simple. If ASP does not support events and NWDir.Search() method uses only events for passing results, then we have to create another "in-the-middle" acting ActiveX component, which will internally call NWDir.Search() and catch the resulting event, but -at the same time-will pass search results as a parameter, without using events. In other words we have to create a kind of ActiveX wrapper for our NWDir control. That makes NWDir search in ASP.
The wrapping control used in this sample is a very simple ActiveX component, created in VB6, which internally utilizes and wraps around the only NWDir.Search() methods. That is enough for our purpose. For any other DS task needed in the Login and Search sample we simply use original NWDir control.
The wrapper can be called NWDirWrapper. Let us first discuss our ASP sample simply by accepting NWDirWrapper as an ActiveX component with described functionality. After that, I will present the code and explanation of how our NWDirWrapper works internally, and how it can be easily created by using VB6.
The whole ASP is split for better understanding and according to the internal logic into three separate ASP files:
LoginAndSearch1.asp - used for object creation and user input during login
LoginAndSearch2.asp - used for login process and for search parameters input
LoginAndSearch3.asp - used for search and for displaying search results
Below in Figure 5 is the data flow in the application:
Data flow of the three ASP files.
The first ASP page is creating an instance of the NWDir and NWSess controls. To be able to "see" and access these created objects from other (different) ASP pages we save references to the created objects into the session variables NWSess1 and NWDir1 using Set session() command. We also create and initiate two other variables - LoginStatus and TreeName, both with the session scope:
<%@ LANGUAGE="VBSCRIPT" %> <!-- FILE: LoginAndSearch1.asp --> <% If IsEmpty(session("NWSess1"))=TRUE Then set session("NWSess1")=Server.CreateObject("NWSessLib.NWSessCtrl.1") set session("NWDir1")=Server.CreateObject("NWDirLib.NWDirCtrl.1") session("LoginStatus")=FALSE session("TreeName")="" End If %>
The entire code block that follows does nothing but accept three input values: tree name, user name, and password and send them to the ASP document specified in <FORM> tag:
<HTML> <HEAD> <TITLE> Login & Search ASP sample code </TITLE> </HEAD> <BODY BGCOLOR=#66CDAA> <Font face="Arial" size="+1" color="Red"> Login & Search ASP sample code </Font> <p> Please login into the system: <p> <FORM ACTION="LoginAndSearch2.asp" METHOD="POST"> <TABLE> <TR> <TD ALIGN="right">DS tree name:</TD> <td><INPUT TYPE="Text" NAME="treeName"></td> </TR> <TR> <TD ALIGN="right">Object FD name:</TD> <td><INPUT TYPE="Text" NAME="objectName"></td> </TR> <TR> <TD ALIGN="right">Password:</td> <TD><INPUT TYPE="Password" NAME="password"></td> </TR> <TR> <TD></TD> <TD> <INPUT TYPE="Submit" NAME="LoginButton" VALUE="Login"> <INPUT TYPE="reset"> </TD> </TR> </TABLE> </FORM> </BODY> </HTML>
This first ASP page produces the following output in the browser window:
The login screen.
After the user enters their login data and presses the Login button, the login information is sent to the second ASP page:
<%@ LANGUAGE="VBSCRIPT" %> <!-- FILE: LoginAndSearch2.asp --> <HTML> <HEAD> <TITLE> Login & Search ASP sample code </TITLE> </HEAD> <SCRIPT LANGUAGE=VBScript RUNAT=Server> Sub DoLogin Dim NWSess1, retCode, treeName, objectName, password Set NWSess1=session("NWSess1") treeName=Request.Form("treeName") objectName=Request.Form("objectName") password=Request.Form("password") On Error Resume Next retCode=FALSE retCode = NWSess1.Login("NDS:\\"+treeName,objectName, password,FALSE) if retCode = TRUE then session("LoginStatus")=TRUE session("TreeName")=treeName End If End Sub </SCRIPT>
The ASP scripting procedure DoLogin() (above) is responsible for logging in to the DS tree. First using the Form property of the Request built-in ASP object, the login data are retrieved into local variables. Then NWSess1.Login() method is called. If this method returns TRUE, then we have successfully processed login in to the system. If this method returns FALSE, it does not necessarily means we are not logged into the DS tree as specified in input parameters. The NWSess.Login() methods returns FALSE not only in case the login has failed (for example due to invalid login parameters), it also returns FALSE in case we are already logged in to the specified DS tree, for example from any previous NWSess.Login() call. So we set session variables LoginStatus and TreeName only in case NWSess.Login() returns TRUE. (They both may be already set from any previous login.)
The logic implemented in the procedure DoLogin() has one advantage and one disadvantage. The advantage is that after the first successful login we can go back and forth between the pages in our application and we do not care any longer about login. After the first successful login we can carelessly return in our browser to this page and press Login button without supplying any parameter and nothing bad will happen. We will remain logged in and this information will be carried in the LoginStatus and TreeName session variables.
The minor disadvantage is that we cannot log in from this application again as a different user. For those of you who would rather be logged in again and again whenever you step into this page, here is the work around: you should submit NWSess.Logout() method with a given DS tree name as parameter prior to calling NWSess.Login() to make sure you are not already logged in as any user to the given DS tree.
The procedure DoLogin() is called outside the <BODY> block. If the session variable LoginStatus is set to TRUE, we can proceed and offer a search form to our user. In case LoginStatus variable is FALSE we have to inform the user that his login has failed. Please notice also the name of the ASP page specified in ACTION parameter within the <FORM> tag. This should be the name of the page that will be run on the server after the user fills in required information and presses the Submit button. Yes, it is our third ASP page that will be responsible for processing search and displaying search results:
<% Call DoLogin %> <BODY BGCOLOR=#66CDAA> <p> <% if session("LoginStatus")=TRUE Then %> <Font face="Arial" size="+1" color="Red"> Login & Search ASP sample code<BR> Search interface </Font> <FORM ACTION="LoginAndSearch3.asp" METHOD="POST"> <TABLE> <TR> <TD ALIGN="right">Object name:</TD> <td><INPUT TYPE="Text" NAME="objectName" SIZE=40></td> </TR> <TR> <TD ALIGN="right">Object type:</TD> <td><INPUT TYPE="Text" NAME="objectType" SIZE=40></td> </TR> <TR> <TD> </TD> <TD> </TD> </TR> <TR> <TD> </TD> <TD> <INPUT TYPE="Submit" NAME="Search" VALUE="Search"> <INPUT TYPE="reset"> </TD> </TR> </TABLE> <% else %> <Font face="Arial" size="+1" color="Red"> Attempt to login has FAILED ! </Font> <p> Please go back and try again... <% End If %> </BODY> </HTML>
Here is what the search form looks like:
The Search interface.
The NWDir.Search() method used internally in our wrapping object requires five parameters:
The name of the object to search for.
The type of the object to search for.
The search scope.
The context where to start the search.
The type of search - asynchronous or synchronous.
Our wrapping ActiveX control only has the method Search() and the property FullName() to make it quick and easy to implement. Our new Search() method simply uses the same first four parameters as the original NWDir.Search() method does. This keeps our new control compatible and also easy to be used. The original fifth parameter is replaced in our own Search() method by a variant variable through which we will return search results back to the calling process.
Here is the code for the third ASP page:
<%@ LANGUAGE="VBSCRIPT" %> <!-- FILE: LoginAndSearch3.asp --> <SCRIPT LANGUAGE=VBScript RunAt=Server> Const SEARCH_SUBORDINATES = 1 Const SEARCH_SUBTREE = 2 Sub DoSearch Dim retCode, objectName, objectType, treeName Dim nwdirWrapper, results ObjectName=Request.Form("objectName") objectType=Request.Form("objectType") treeName=session("TreeName") On Error Resume Next set nwdirWrapper=Server.CreateObject("NWDirWrapperLib.NWDirWrapper") call nwdirWrapper.Search(objectName,objectType,SEARCH_SUBTREE,_ "NDS:\\"+treeName+"\[Root]",results) If UBound(results) then For Each result In results response.write(result & "<BR>") Next Else If results(0)<> Empty Then response.write(results(0) & "<BR>") Else response.write "No objects found !<BR>" End If End If nwdirWrapper=Nothing End Sub </SCRIPT> <HTML> <HEAD> <TITLE> Login & Search ASP sample code </TITLE> </HEAD> <BODY BGCOLOR=#66CDAA> <Font face="Arial" size="+1" color="Red"> Login & Search ASP sample code<BR> Search results: </Font> <p> <% DoSearch %> </BODY> </HTML>
In the <BODY> of the document we call the DoSearch() scripting procedure, which runs on the ASP server. This procedure first reads parameters entered by the user in the previous HTML document -objectName and objectType. Using the session variable TreeName the full DS name of the starting context is constructed; it is "NDS:\\TREE_NAME\[Root]" in our sample because the search starts for simplicity from the [Root], and all parameters are passed to our NWDirWrapper.Search() method. The last parameter of this method results is a variant that will bring back search results. As you will see later, when discussing implementation of our new Search(), this method is blocking, so it will not return before the DS search has internally finished. Search results (if there are any) are then sent to the user HTML document with a simple Response.Write() command. The last command in DoSearch() procedure makes obligatory final cleaning and destroys our wrapping object.
Below is the browser window produced by our third ASP page code in case there are some objects found during the search:
The search results.
How is the NWDirWrapper.cls implemented? Here is the Visual Basic code:
Public Enum NWDirWrapperSearchScope SEARCH_SUBORDINATES = 1 SEARCH_SUBTREE = 2 End Enum Public Enum NWDirWrapperSearchMode SEARCH_SYNCHRONOUS = 0 SEARCH_ASYNCHRONOUS = 1 End Enum Private mvarSemaphore As Integer Private mvarResults As Variant Private WithEvents mvarNWDir1 As NWDirlib.NWDir Public Sub mvarNWDir1_SearchCompleted(ByVal Results As Variant) mvarResults = Results mvarSemaphore = 1 End Sub Public Sub Search(ByVal NameFilter As String, ByVal ClassFilter As String, _ ByVal Scope As NWDirWrapperSearchScope, _ ByVal StartContext As String, searchResults As Variant) mvarSemaphore = 0 Call mvarNWDir1.Search(NameFilter, ClassFilter, Scope, StartContext, SEARCH_SYNCHRONOUS) Do DoEvents Loop While mvarSemaphore = 0 searchResults = mvarResults End Sub Private Sub Class_Initialize() On Error Resume Next Err.Clear Set mvarNWDir1 = CreateObject("NWDirLib.NWDirCtrl.1") If Err.Number Then Unload Me End If End Sub Private Sub Class_Terminate() Set mvarNWDir1 = Nothing End Sub
Notice that our NWDirWrapper class creates and uses its own copy of NWDir control. This is fully correct from the access rights point of view; both this new NWDir copy and the original NWDir object created in the ASP page have the same level of access rights to the DS tree, because both are created and handled in the ASP engine context of the same NT account.
Our own Search() method simply sets value of local mvarSemaphore variable to zero and then calls the original NWDir.Search() function with SEARCH_SYNCHRONOUS parameter specified. When NWDir.Search() method finishes searching the DS tree and returns to our calling function, we wait for a while in the following loop:
Do DoEvents Loop While mvarSemaphore = 0
Why are we looping? Wasn't the NWDir.Search() call we issued a synchronous one? Yes-but we simply want to be 100% sure that the search results, which are sent back to the NWDir_SearchCompleted() event, are saved to our variable mvarResults before we pass them back to the calling ASP scripting procedure DoSearch().
Now we have a functional search in our ASP but our code has two drawbacks. First, we start in our implementation search always from the [Root] of the DS tree. And second, our implemented NWDirWrapper.Search() method is blocking. Both drawbacks can cause the search to take quite a long time in a tree with thousands of objects.
Fortunately there is a solution for both drawbacks. We can add the third input parameter to the search window and let the user decide which tree context to start the search from. We can also code our wrapper to search asynchronously and cancel the search process if necessary.
Using the New NWDirQuery Control
Novell AX engineering has recently recognized the importance of the search issue and created a new AX control called NWDirQuery. This control can be used for searching in ASP applications; it also supports the latest NDS8 search capabilities. Our previous ASP search sample code, which uses a wrapping technique, can be easily modified to use this NWDIrQuery control. Basically only two modifications are needed:
Modification in LoginAndSearch2.asp-because the filter is set differently for NWDirQuery, we don't need to have two separate inputs-objectType and objectName-like we used in wrapping the sample code. Only one input is required here, and we name it objectFilter.
Modification in LoginAndSearch3.asp-instead of creating NWDirWrapper object we create NWDirQuery object here -nwdirQ, next we set required parameters, and finally we call Search method. Below is the substantial part of this modification:
<%@ LANGUAGE="VBSCRIPT" %> <!-- FILE: LoginAndSearch3.asp --> <SCRIPT LANGUAGE=VBScript RunAt=Server> Const SEARCH_SUBORDINATES = 1 Const SEARCH_SUBTREE = 2 Sub DoSearch Dim retCode, objectFilter, treeName Dim nwdirQ, results, result ObjectFilter=Request.Form("objectFilter") treeName=session("TreeName") On Error Resume Next set nwdirQ=Server.CreateObject("NWDirQueryLib.NWDirQuery.1") nwdirQ.FullName="NDS:\\"+treeName+"\[Root]" nwdirQ.maximumResults=100 nwdirQ.SortKeys="CN" nwdirQ.Filter=objectFilter nwdirQ.SearchMode=0 ' synchronous ! nwdirQ.SearchScope=2 set results=nwdirQ.Search For each result in results response.write(result.FullName & ", " & result.Layout & "<BR>") Next Set nwdirQ=Nothing End Sub </SCRIPT>
The output of this modified sample is pretty similar to the code that used the wrapping technique.
Now you can verify remote users against NDS with ASP. Part 5 will discuss browsing the DS tree remotely.
* Originally published in Novell AppNotes
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.