Novell Home

AppNote: Designing and Building Applications on Directories

Novell Cool Solutions: AppNote
By Michel Bluteau

Digg This - Slashdot This

Posted: 4 May 2005
 

Introduction

This article is based on my research and experimentations with C#.NET applications design on top of LDAP directories, such as Novell eDirectory. While most application design in the .NET world happens on top of an SQL database - mainly Microsoft SQL Server on the Windows platform - I am taking a different approach, leveraging LDAP directories when designing applications.

There are many advantages associated with leveraging a directory instead of a database, including built-in security, identity management, RBAC (Role Base Access Control) or RBS (Role Base Services in iManager), portability, etc. If Web Services applications are built directly on top of the directory, then the synchronization between the directory and the database hosting the application is no longer needed.

Here are the objectives for this article:

  1. Leveraging a flexible ADO object to interface with LDAP directories or eDirectory
  2. Providing an introduction to C#.NET Visual Web Application development using Microsoft Visual Studio.NET
  3. Interfacing with the LDAP directory for authentication
  4. Interfacing with the LDAP directory for searching and accessing information
  5. Interfacing with the LDAP directory in order to modify information
  6. Passing parameters inline with the URL, between forms
  7. Moving the application to a Linux machine using Mono and XSP

These objectives should provide you with the toolset required to move forward and design complex applications. The application discussed in this AppNote is included for download (LdapApplication1.zip), as well as the Visual Studio.NET project (LdapApplication1_VSProject.zip). The application can be executed on Windows 2003 with the .Net Framework, or on Linux or Mac with Mono and XSP. You simply need to uncompress the files to a local volume and load the application. An LDAP directory must be available on the network.

1: Flexible ADO object to interface with LDAP directories or eDirectory

The Windows 2003 .NET framework provides a class for interfacing with Active Directory (System.DirectoryServices.dll). But this class is limited in the way it interfaces with objects in other LDAP directories, or their attributes. We will instead leverage a class that has been developped by Novell under the Mono initiative - Novell.Directory.LDAP.dll. For more information about this class or to download it, visit the following URLs:

Using .NET C# LDAP Library
/coolsolutions/feature/11204.html

Project: LDAP Libraries for C#
http://forge.novell.com/modules/xfmod/project/showfiles.php?group_id=1318

We will use Visual Studio.NET in order to design our application. While we could use Monodevelop or another IDE, Visual Studio.NET is still the one I find the most interesting to use today - maybe because I have been using it for a while. Monodevelop is evolving very rapidly. Soon it will make it possible to combine different languages like C#, Java and Python in a single application, so I am seriously thinking about switching to Monodevelop in the near future. Running Java Beans side by side with C# is also an interesting concept. Anyway, the resulting application can be copied to a Linux server with Mono and XSP and run as is.

Now let's start designing and developing the application.

2: Introduction to C#.NET Visual Web Application Development using Microsoft Visual Studio.NET

Here is the Visual Studio.NET development window:

Figure 2.0: Visual Studio.NET 2003 on Windows Server 2003

Larger image

Setting Up the Project

1. Select New Project and choose Visual C# Projects, ASP.NET Web Application.

Figure 2.1: New Project wizard

2. Name the project LdapApplication1.

Figure 2.2: New application LdapApplication1

Larger image

3a. Delete the default web form WebForm1.

3b. Select WebForm1.aspx from Solution Explorer (on the right), right-click the form and select delete.

Figure 2.3: Deleting the default web form WebForm1

4a. Right-click the LdapApplication1 object in Solution Explorer, select Add.

4b. Select Add Web Form and create a Web Form called Login.aspx.

Figure 2.4: Creating Web Form Login.aspx

Figure 2.4a: Newly created web form Login.aspx

Larger image

Now you need to change the form layout to FlowLayout, which is more the standard for web applications.

5. Click inside the layout for Login.aspx, then select pageLayout in the Properties and change the value to FlowLayout.

Figure 2.5: Changing the layout for Login.aspx to FlowLayout

Larger image

6. Next, select Toolbox from the View menu.

Figure 2.6: Design with Toolbox

Larger image

7a. Position the cursor inside the form and press Enter.

7b. Type some text in order to explain to the user what to do with this form, then press Enter again.

Figure 2.7: Typing text inside the form

Larger image

8. Select HTML mode (versus Design), and you will be able to see the HTML code generated automatically by VS.NET.

Figure 2.8: HTML code generated by VS.NET

Larger image

9. Now select a Table object from the Toolbox, under HTML, and drag-and-drop it inside the form, below the text.

Figure 2.9: HTML Table object

Larger image

10a. Right-click the Table inside the last row, and select Insert, Row below, twice.

10b. Position yourself in the last column, right-click and select Delete, Column.

10c. Type "Server Address:" in the first column, first row, then "Context:" in the cell below it, then "Username:" in the cell below that, then finally Password: in the next cell. You may want to reajust the width for the column with your mouse.

10d. Drag and drop three Text Field controls into the cells in the second column for the first three rows, and a Password Field control for the cell below it.

10e. Add a button in the last cell for column one and set its value property to Login.

10f. Remove the cell to the right of the button (right-click, Delete Cell).

10g. Select the cell containing the button and set its colspan property to 2.

Now the Login.aspx form should look like this:

Figure 2.10: Using a Table HTML control to design the Login.aspx web form

Larger image

11a. Right-click the Login.aspx object in Solution Explorer and select Set as Start Page.

11b. Press CTRL-F5 compile the project

11c. Access the project in Internet Explorer.

Figure 2.11: Web Application, after adding controls to the Login.aspx form

Adding Visual Styles

So far, we have not done much - the results are pretty rough. We could add visual elements inside the forms directly, but instead we will leverage Visual Styles, through the use of a CSS (Cascading Style Sheet).

12a. Go back to the project and create a subfolder called Styles in Solution Explorer under LdapApplication1. To do this, right-click and select Add folder.

12b. Right-click the subfolder Styles, select Add New Item, and select Style Sheet.

12c. Name the style sheet as Style1.css:

Figure 2.12: Creating a Style Sheet

You should now see an empty Style Sheet in VS.NET, with just a body { } element.

13a. Right-click the body element and select Build Style. You should now see the Style Builder.

13b. Select the Background category in the Style Builder and select a background color, such as Silver. You can select a Font Family under the Font category, as well as other options related to Fonts.

13c. Click OK when you are finished.

Figure 2.13: Style Builder

14a. Right-click in the Style Sheet and select Add Style Rule.

14b. Select Table in the Element drop-down list and click OK.

Figure 2.14: Add Style Rule

15. Right-click the new TABLE element in the Style Sheet, and select Build Style. You can select the same Font Family and size as above, or a different combination.

Figure 2.15: Style Sheet with a TABLE element

Larger image

16a. Select the form Login.aspx.

16b. Drag and drop the Style1.css StyleSheet onto it from Solution Explorer, and the style you just defined will be applied to the web form. You can look at the form in HTML mode to see that the css StyleSheet has been added to the code.

Figure 2.16: Login.aspx form after the Style1 css StyleSheet has been applied to it

Larger image

17a. Go back to the Style1.css Style Sheet, right-click inside it and select Add Style Rule.

17b. Instead of selecting an element from the drop-down list, select Class Name and type "TextBox" - then click OK.

17c. Type the following text inside the .TextBox rule:

border-right: #c7ccdc 1px solid;
border-top: #c7ccdc 1px solid;
border-left: #c7ccdc 1px solid;
border-bottom: #c7ccdc 1px solid;
font-size: 10pt;
font-family: 'Times New Roman', Tahoma, Verdana;

17d. Then copy and paste the .TextBox rule, change the name to .Button, and modify the content to match the following:

background-color: Lime;
border-right: darkgray 1px solid;
border-top: darkgray 1px solid;
border-left: darkgray 1px solid;
border-bottom: darkgray 1px solid;
font-size: 10pt;
font-family: 'Times New Roman', Tahoma, Verdana;

Figure 2.17: Adding the .TextBox and .Button rules

Larger image

18a. Open the Login.aspx form in Design view.

18b. Select the four input boxes and set their class property to TextBox.

18c. Set the Button's class property to Button.

18d. Compile and run the page again, and you will see the changes:

Figure 2.18: Page view after adding the .TextBox and .Button rules

Figure 2.18a: Creating a new folder named Controls under LdapApplication1

Adding Headers and Footers

Now we will add some standard footers and headers to our application, leveraging User Controls to do so.

19a. Create a new folder called Controls under LdapApplication1 in Solution Explorer (see the figure below).

19b. Right-click the Controls folder and select Add Web User Control.

Figure 2.19: Creating a Web User Control named LdapAppHeader.ascx

20a. Drag and drop a Panel Web Forms Control from the Toolbox onto the page.

20b. Set its CssClass Property to HeaderLdapApp.

20c. Set the text inside the Panel to LDAP Application.

20d. Drop an image Web Forms Control inside the panel.

20e. To the right of the text, set its ID Property to ImageLdapApp, CssClass to HeaderImage, and ImageUrl to an image of your choice.

20f. You can create a folder called Images under the application and copy images files into it using a drag and drop with a right-click from Windows Explorer.

20g. Drop another Panel under the previous one, delete the text inside it, and set is CssClass to HeaderTitle.

20h. Drop an image on this second panel, set its ID property to ImageConn, its CssClass to HeaderImage, and its ImageUrl to an image of your choice.

20i. Drop a label inside the second panel, to the right of the image, and set its ID to LabelConn and Text to "Welcome!".

Now your Control should look like this:

Figure 2.20: User Control with 2 Panels

We have referenced some CSS styles that have not been defined yet, HeaderLdapApp, HeaderImage, and HeaderTitle - so let's do that now.

21. Update the Style1 stylesheet. Notice that HeaderTitle is identical to HeaderLdapApp, except for the text-align:right which must be removed.

Figure 2.21: Three new CSS Styles added to Style1.css

22. Go back to the Login.aspx page and drag the LdapAppHeader.ascx Control from Solution Explorer onto it:

Figure 2.22: Adding the User Control LdapAppHeader.ascx to Login.aspx

23. Press Ctrl-F5 to run the application, and you should get this:

Figure 2.23: Web form with a User Control used as a header

To add a footer,

24a. Right-click the Controls folder in Solution Explorer.

24b. Select Add Web User Control and name it LdapAppFooter.ascx.

24c. Add a Panel to it and set its CssClass to FooterLdapApp.

24d. Switch to HTML mode and change the code for the Panel to the following:

<asp:Panel id="PanelFooter" runat="server" CssClass="FooterLdapApp"> LDAP Application - C#.NET and LDAP together
<b>Example application</b> </asp:Panel>

24e. Create a FooterLdapApp class in Style1.css like the following:

.FooterLdapApp
{
  background-color: #336699;
  text-align: center;
  color: white;
  width: 100%;
  font-size: 10pt;
  font-family: 'Times New Roman', Tahoma, Verdana;
}

24f. Now add the Footer to the Login.aspx page (drag-and-drop).

Figure 2.24: Adding the User Control LdapAppFooter to Login.aspx

Figure 2.24a: Running the application with the Footer

Creating a SubHeader Custom Control

25a. Right-click LdapApplication1 and select Add, Add Class.

25b. Name the class as SubHeader.cs:

Figure 2.25: Creating a Custom Control called SubHeader.cs

26a. Replace the content of SubHeader.cs with the following:

using System;
using System.IO;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace LdapApplication1
{
	/// 
	/// Summary description for SubHeader.
	/// 
	public class SubHeader : WebControl
	{
		public SubHeader()
		{
			// Initialize default values
			this.Width = new Unit(100, UnitType.Percentage);
			this.CssClass = "SubHeader";
		}
		// Property to allow the user to define the URL for the login page
		public string LoginUrl
		{
			get { return _login; }
			set { _login = value; }
		} string _login = string.Empty;
		// This method is called when the control is being built
		protected override void CreateChildControls()
		{
			Label lbl;
			HyperLink login = new HyperLink();

			if (_login == string.Empty)
			{
				login.NavigateUrl = Context.Request.ApplicationPath +
					Path.AltDirectorySeparatorChar + 
					Path.AltDirectorySeparatorChar + "Login.aspx";
			}
			else
			{
				login.NavigateUrl = _login;
			}

			if (Context.User.Identity.IsAuthenticated)
				login.Text = Context.User.Identity.Name + " - Edit your profile";
			else
				login.Text = "You need to login";

			this.Controls.AddAt(0,login);

			// Add a couple of blank spaces and a separator character
			this.Controls.Add(new LiteralControl(" - "));

			// Add a label with the current data
			lbl = new Label();
			lbl.Text = DateTime.Now.ToLongDateString();
			this.Controls.Add(lbl);
		}
	}
}

26b. Edit the Login.aspx page in HTML mode and add the following directive at the top of the page:

<%@ Register TagPrefix="ap" NameSpace="LdapApplication1" Assembly="LdapApplication1" %>

26c. Add the following line right under the LdapAppHeader, within the same paragraph(before </P>):

<ap:subheader id="SubHeader1" runat="server"/>

26d. Run the application, and you should get the following:

Figure 2.26: Running the application after adding the SubHeader

Before login, the URL link directs the user to Login.aspx page. Later on, we will be able to leverage these Controls once we start to interface with the LDAP directory.

Creating the Default.aspx Page

We will now create a new page, the Default.aspx page. This page will be the Default page users will see once they are logged in.

27a. Add a new Web Form under the LdapApplication1 object and call it Default.aspx.

27b. Change its pageLayout Property to FlowLayout.

27c. Drop both LdapAppHeader.ascx and LdapAppFooter.ascx onto Default.aspx, while doing a few carriage returns between the two.

27d. Change their IDs to LdapAppHeader2 and LdapAppFooter2.

27e. Add Style1.css to Default.aspx.

27f. Add the SubHeader.cs Custom Control in the same way you did for Login.aspx, but change its id to SubHeader2.

27g. Under the headers, type some text - for example, "Welcome to LdapApplication1 - Illustrating application design with C#.NET and LDAP"

27h. Drop a PlaceHolder Web Control below the text in Default.aspx.

27i. Open the code-behind file (double-click on Default.aspx) and remove the "using System.Drawing;" line.

27j. Add the following code to the Page_Load() method:

Table tb = new Table();
TableRow row;
TableCell cell;
Image img;
HyperLink lnk;

if (Context.User.Identity.IsAuthenticated)
{
	// Create a new blank table row
	row = new TableRow();

	// Set up the Login image
	img = new Image();
	img.ImageUrl = "Images/but_login2.jpg";
	img.ImageAlign = ImageAlign.Middle;
	img.Width = new Unit(24, UnitType.Pixel);
	img.Height = new Unit(24, UnitType.Pixel);

	// Create a cell and add the image
	cell = new TableCell();
	cell.Controls.Add(img);

	// Add the new cell to the row
	row.Cells.Add(cell);

	// Set up the Login link
	lnk = new HyperLink();
	lnk.Text = "Login";
	lnk.NavigateUrl = "Login.aspx";

	//Create the cell and add the link
	cell = new TableCell();
	cell.Controls.Add(lnk);

	// Add the new cell to the row
	row.Cells.Add(cell);

	// Add the row to the table
	tb.Rows.Add(row);

	// Create a new blank table row
	row = new TableRow();
}
else
{
	// Code for unauthenticated users here ...
}
// Finally, add the table to the placeholder
PlaceHolder1.Controls.Add(tb);

27k. Set the start page as Default.aspx, then run the application.

The link we created for Login will not show up yet, because the user is not yet authenticated. It will appear later on once we integrate authentication in our application:

Figure 2.27: Default.aspx page

It is easy to add other links to the Default.aspx page, just by copying the above code once for each new link and modifying the resulting copied block of code. We will do that later.

Adding a Base Class

We have been able to re-use our Controls for this second page, Default.aspx, but it would be nice not to have to include these controls on each and every page we add to our application. There is a way to do just that, through a new class.

28a. Create a new class under the application and name it LdapApplicationBase.cs.

28b. Under {{ using System; }}, add the following:

using System.IO;
using System.Web.UI;
using LdapApplication1.Controls;

28c. Replace the public class LdapApplicationBase code with the following:

public class LdapApplicationBase : System.Web.UI.Page
{
	protected string HeaderMessage = String.Empty;
	protected string HeaderIconImageUrl = String.Empty;

	protected override void Render(System.Web.UI.HtmlTextWriter writer)
	{
		// Get a reference to the form control
		Control form = Page.Controls[1];

		// Create and place the page header
		LdapAppHeader header;
		header = (LdapAppHeader)
			this.LoadControl("~/Controls/LdapAppHeader.ascx");

		header.Message = HeaderMessage;
		header.IconImageUrl = HeaderIconImageUrl;
		form.Controls.AddAt(0, header);

		// Add the SubHeader custom control
		form.Controls.AddAt(1, new SubHeader());

		// Add space separating the main content.
		form.Controls.AddAt(2, new LiteralControl("

")); form.Controls.AddAt(form.Controls.Count, new LiteralControl("

")); // Finally, add the page footer LdapAppFooter footer; footer = (LdapAppFooter) this.LoadControl("~/Controls/LdapAppFooter.ascx"); form.Controls.AddAt(Page.Controls[1].Controls.Count, footer); // Render as usual base.Render(writer); } public LdapApplicationBase() { // // TODO: Add constructor logic here // } }

28d. Open the code-behind page for Default.aspx and change the class declaration to this:

public class _Default : LdapApplicationBase

28e. Remove the Header and Footer User Controls from Default.aspx.

28f. Edit the code-behind page for LdapAppHeader.ascx and add the following code after the block {{ Web Form Designer-generated code }}:

// Accessor method for the message property
	public string Message
	{
		get { return _message; }
		set { _message = value; }
	} string _message = String.Empty;

	// Accessor method for the IconImageUrl property
	public string IconImageUrl
	{
		get { return _imagel; }
		set {_imagel = value; }
	} string _imagel = String.Empty;

	// Populate the controls with the property values
	protected override void Render(System.Web.UI.HtmlTextWriter writer)
	{
		if (Message != String.Empty)
			this.LabelConn.Text = Message;
		if (IconImageUrl != String.Empty)
			this.ImageConn.ImageUrl = IconImageUrl;
		base.Render(writer);
	}

28g. Remove the following two items from Default.aspx:

<%@ Register TagPrefix="ap" NameSpace="LdapApplication1" Assembly="LdapApplication1" %>

<ap:subheader id="SubHeader1" runat="server"/>

28h. Run the application, and you should see that Default.aspx still contains the User Controls:

Figure 2.28: Default.aspx page with LdapApplicationBase.cs

Of course, it is probably a good idea to go through the same steps for Login.aspx, i.e. to remove the Header, Footer, and SubHeader, and leverage LdapApplicationBase.cs.

Now that we have a visual framework for our application, it is time to interface with the LDAP directory. In our example, we will leverage Novell eDirectory, but the application could be used against any LDAP directory. We will leverage ADO.NET, but instead of interfacing with a database, as in most textbook examples, we will interface with our LDAP directory.

3. Interfacing with the LDAP directory for authentication

ADO.NET is a data access strategy for .NET applications. ADO originally stood for ActiveX Data Objects, but it's now just as an acronymn, since ActiveX has been evacuated from the Microsoft strategy. It was originally designed to provide an interface with various data providers, including SQL Server, Oracle, OLE DB, ODBC, etc. We refer to ADO.NET through a series of classes; for example, System.DirectoryServices is Microsoft's class for interfacing with Active Directory.

While System.DirectoryServices can be used to interface with another LDAP directory, it is a bit limited in terms of the objects it can manage, or the attributes.

Novell, through the Mono initiative, is making available a class called Novell.Directory.Ldap. This class can be used against Novell eDirectory and other LDAP directories such as OpenLDAP, Oracle Internet Directory, etc.

We will now start to leverage Novell.Directory.Ldap from within our application.

1. Downloading and copy Novell.Directory.Ldap.dll to your .NET server:

Figure 3.1: Downloading Novell.Directory.Ldap

2. Right-click References in Solution Explorer and add Novell.Directory.Ldap.

Figure 3.2: Adding Novell.Directory.Ldap

Figure 3.2a: Novell.Directory.Ldap class included as a reference

The first page/form we will start to modify is Login.aspx.

3. Add a new import for Novell.Directory.Ldap to the code-behind file for Login.aspx.

Figure 3.3: Adding the Novell.Directory.Ldap import

4. Add two lines for IconImageUrl and HeaderMessage in the Page_Load event handler for Login.aspx.cs.

Figure 3.4: Updated Page_Load event handler for Login.aspx.cs

5. Go to the Login.aspx form in Design mode. Select each TextBox and the Password fields and set an ID for them, in the following way:

Server Address: ServerAddress
Context: UserContext
Username: UserName
Password: UserPassword
Login: LoginButton
Larger image

Figure 3.5: Setting an id for TextBox object Server Address

6a. Right-click these elements and select Run as Server Control, so you can reference them in the code-behind file. You will notice that a little green arrow now appears for each element in the upper-left corner.

6b. Add an HTML Text Field to the left of ServerAddress.

6c. Set its id to ServerPort, and its class to TextBox.

6d. Right-click it and select Run as Server Control. This new control is required if we want to select a port other than the default port for LDAP (389), which would need to be hardcoded in the code-behind file.

Figure 3.6: Code-behind file for Login.aspx after the elements have been turned into server controls

7a. From the Login.aspx page in Design mode, drop a Label control under the table.

7b. Clear its Text property and set its ID to LabelMessage.

7c. Set its CssClass property to Normal.

7d. Drop a Panel control after this Label and include a carriage return.

7e. Set its ID to PanelError and its Visible Property to False.

7f. Clear the text.

7g. Drop a Label control onto this Panel, set its ID to LabelError, its CssClass to Normal, it ForeColor to Red, then clear its Text.

The form should now look like this:

Figure 3.7: Login.aspx after adding the controls, including ServerPort

We also need to edit the Style1.css and add a Normal section like this one:

.Normal
{
  font-size: 10pt;
  font-family: 'Times New Roman', Tahoma, Verdana;
}

8a. Go back to the Login.aspx page, and double-click the Login button. You'll be taken to the code-behind file, in a new event handler called LoginButton_ServerClick.

8b. Replace the content for the event handler with the following:

private void LoginButton_ServerClick(object sender, System.EventArgs e)
{
			
	try

	{
		LdapConnection LdapConn = new LdapConnection();
		LdapConn.Connect(ServerAddress.Value,Int32.Parse(ServerPort.Value));
				LdapConn.Bind("cn="+UserName.Value+","+UserContext.Value,UserPassword.Value);

		string id = UserName.Value;

		// Save password for re-use in other forms
		Session["gPasswd"] = UserPassword.Value;
			
		if (id != null)
		{
			// Set the user as authenticated and send him to the
			// page originally requested.
			// FormsAuthentication.RedirectFromLoginPage(id,CheckPersist.Checked);
		}
		else
		{
			this.PanelError.Visible = true;
			this.LabelError.Text = "Invalid user name, password or server!";
		}


		this.LabelMessage.Text = "Welcome " + this.UserName.Value;

	}
	catch (LdapException ex)
	{
		// Exception is thrown to go to next entry
		this.PanelError.Visible = true;
		this.LabelError.Text = ex.LdapErrorMessage;
	}

}

8c. Add the following include for the Login.aspx.cs file:

using System.Web.Security;

8d. Modify the Web.config file to enable Forms Authentication:

Figure 3.8: Setting the authentication mode to Forms in Web.config

9a. Go into the Administrative tools for Windows, and start the Services Control Panel.

9b. Change the Properties for ASP.NET State Service to Automatic, and start it:

Figure 3.9: ASP.NET State Service

10. Run the application now, and you will perform a login against the LDAP directory for the Login.aspx page:

Figure 3.10: Running the application to log in

You will very likely see an error at the bottom of the page (e.g. -601) but this is normal for now, since we need to add some more logic.

Adding a CheckPersist Checkbox Control

11a. Go back to Login.aspx in Design mode.

11b. Click in the row where the Login button is.

11c. Right-click and select Insert, then Row Above.

11d. Drag and drop a CheckBox Web Forms Control inside this new row.

11e. Give it an ID of CheckPersist, and change the Text to "Remember me on this machine."

Figure 3.11: CheckPersist CheckBox Control

12a. Go back to the code-behind file for Login.aspx and uncomment the following line:

FormsAuthentication.RedirectFromLoginPage(id,CheckPersist.Checked);

12b. Open web.config and change the Authorization section to the following:

<deny users="?" />

12c. In Login.aspx, save the password to a session variable. You can re-use it re-establish the connection behind the scenes when you switch forms within the application, as follows:

Session["gpasswd"] = UserPassword.Value;

12d. Make the following change in Login.aspx to re-use this password, together with the identity. That way, other forms can continue to access the LDAP directory with the same credentials that were used for the authentication:

ldapConn.Bind(Context.User.Identity.Name,Session["gPasswd"].ToString() );

Now the application is secured, and only authenticated users will have access to it.

12e. Set the default page to Default.aspx and start the application.

You will be redirected automatically to the Login.aspx page, and if you authenticate, then you will be redirected to the Default.aspx web page:

Figure 3.12: Redirection to the Default.aspx page once authenticated

Note that the second header now says "admin - Edit your profile" instead of "you need to log in", and that the Placeholder now appears in the page for the Login link.

Managing a Session Token while Moving between Different Forms in the Application

Now we will modify Login.aspx so it saves all important values to global session variables.

13. Save the information you entered for Login.aspx into global variables.

Figure 3.13: Login.aspx with saved global variables

4. Interfacing with the LDAP directory for searching and accessing information

1a. Create a new Web Form called BrowseTree.aspx and change its pageLayout Property to FlowLayout.

1b. Drag and drop Style1.cs.

1c. Add the include for the Novell.Directory.Ldap class.

1d. Drag-and-drop a Web Forms table element and name it tbUsers. You can set its ForeColor to Blue.

Figure 4.1: BrowseTree Web Form

Larger image

2a. Go to the code-behind file for BrowseTree.aspx.cs and namespace to LdapApplication1, and change the references to LdapApplicationBase:

namespace LdapApplication1
{
	/// 
	/// Summary description for BrowseTree.
	/// 
	public class BrowseTree : LdapApplicationBase
	{
		protected System.Web.UI.WebControls.Table tbUsers;

2b. Type in the code to generate the tbUsers table, line by line, based on a LDAP search for Users against the LDAP directory. We are now leveraging the Session global variables created through Login.aspx.cs. We are interested attributes dn, cn, givenname and sn, as shown below:

private void Page_Load(object sender, System.EventArgs e)
{

	//DataTable for users
	// Table tbUsers = new Table();
	TableRow row;
	TableCell cell;
	Label lbl;
	String cn;
	String sn;
	String givenname;
	HyperLink lnk;

	// Creating an LdapConnection instance 
	LdapConnection ldapConn= new LdapConnection();

//Connect function will create a socket connection to the server
ldapConn.Connect(Session["gAddress"].ToString(),Int32.Parse(Session["gPort"].ToString()));

//Bind function will Bind the user object Credentials to the Server
ldapConn.Bind("cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString(),Session["gPasswd"].ToString());

	// Searches in the Marketing container and return all child entries just below this
	//container i.e Single level search
	LdapSearchQueue queue=ldapConn.Search ( Session["gContext"].ToString(),
		LdapConnection.SCOPE_SUB,	
		"(objectClass=User)",				
		null,		
		false,
		(LdapSearchQueue) null,
		(LdapSearchConstraints) null );

	LdapMessage message;

	row = new TableRow();

	lbl = new Label();
	lbl.Text = "dn";
	cell = new TableCell();
	cell.Controls.Add(lbl);
	// Add the new cell to the row
	row.Cells.Add(cell);
	lbl = new Label();
	lbl.Text = "cn";
	cell = new TableCell();
	cell.Controls.Add(lbl);
	// Add the new cell to the row
	row.Cells.Add(cell);
	lbl = new Label();
	lbl.Text = "givenname";
	cell = new TableCell();
	cell.Controls.Add(lbl);
	// Add the new cell to the row
	row.Cells.Add(cell);
	lbl = new Label();
	lbl.Text = "sn";
	cell = new TableCell();
	cell.Controls.Add(lbl);
	// Add the new cell to the row
	row.Cells.Add(cell);
	tbUsers.Rows.Add(row);

	while ((message = queue.getResponse()) != null)
	{
		cn = "null";
		sn = "null";
		givenname = "null";
		if (message is LdapSearchResult)
		{
			LdapEntry entry = ((LdapSearchResult) message).Entry;
			LdapAttributeSet attributeSet = entry.getAttributeSet();
			System.Collections.IEnumerator ienum = attributeSet.GetEnumerator();
			row = new TableRow();
			while(ienum.MoveNext())
			{
				LdapAttribute attribute=(LdapAttribute)ienum.Current;
				string attributeName = attribute.Name;
				string attributeVal = attribute.StringValue;
				if (attributeName == "cn") { cn = attributeVal; }
				if (attributeName == "sn") { sn = attributeVal; }
				if (attributeName == "givenName") { givenname = attributeVal; }
			}
			// Set up the Login link
			lnk = new HyperLink();
			lnk.Text = entry.DN;
			lnk.NavigateUrl = "ModifyUser.aspx?dn=" + entry.DN;

			//Create the cell and add the link
			cell = new TableCell();
			cell.Controls.Add(lnk);

			// Add the new cell to the row
			row.Cells.Add(cell);

			lbl = new Label();
			lbl.Text = cn;
			cell = new TableCell();
			cell.Controls.Add(lbl);
			// Add the new cell to the row
			row.Cells.Add(cell);
			lbl = new Label();
			lbl.Text = givenname;
			cell = new TableCell();
			cell.Controls.Add(lbl);
			// Add the new cell to the row
			row.Cells.Add(cell);
			lbl = new Label();
			lbl.Text = sn; 
			cell = new TableCell();
			cell.Controls.Add(lbl);
			// Add the new cell to the row
			row.Cells.Add(cell);
			// Insert the row
			tbUsers.Rows.Add(row);
			// ds.Tables["users"].Rows.Add(topRow);				
		}
	}

}

You can see that our table also includes a link to another form not yet created, ModifyUser.aspx. We will get to that next; but before that,

2c. Run your new form by setting it as the default page, and execute the application.

Figure 4.2: Running the application after the creation of the BrowseTree form

Figure 4.2a: The BrowseTree form, using dn as a URL link in the first column.

5. Interfacing with the LDAP directory in order to modify information

1a. Create the new form ModifyUser.aspx and set its layout to FlowLayout.

1b. Drag and drop Style1.cs.

1c. Add the include for the Novell.Directory.Ldap class.

1d. Drag-and-drop a Web Forms table element and name it tbMU.

1e. Like you did previously for Login.aspx, add text and TextBoxes (CssClass set to TextBox) to the table.

1f. Change the number of lines and columns to our liking, add a Button, and change the look and feel so you are happy with the results. The following Web Forms elements should be inside the table:

  • A TextBox called TextLogin;
  • A TextBox called TextPassword with TextMode set to Password
  • A TextBox called TextFName
  • A TextBox called TextLName
  • A TextBox called TextDept
  • A TextBox called TextTitle
  • A Button called ButtonAccept with its text set to Accept
  • A LabelMessage called LabelMessage, with its Text cleared

You should now get something similar to the following:

Figure 5.1: ModifyUser.aspx Web Form.

2. Go to the code behind page and make the usual changes:

using Novell.Directory.Ldap;

namespace LdapApplication1
{
	/// <summary>
	/// Summary description for ModifyUser.
	/// </summary>
	public class ModifyUser : LdapApplicationBase
	{
		protected System.Web.UI.WebControls.TextBox TextLogin;
		protected System.Web.UI.WebControls.TextBox TextPassword;
		protected System.Web.UI.WebControls.TextBox TextFName;
		protected System.Web.UI.WebControls.TextBox TextLName;
		protected System.Web.UI.WebControls.TextBox TextDept;
		protected System.Web.UI.WebControls.TextBox TextTitle;
		protected System.Web.UI.WebControls.Label LabelMessage;
		protected System.Web.UI.WebControls.Button ButtonAccept;

6. Passing Parameters Inline with the URL, between Forms

If you go back to BrowseTree.aspx.cs, you should have the following HyperLink for the dn column:

lnk.NavigateUrl = "ModifyUser.aspx?dn=" + entry.DN;

We will now leverage the information sent inside the URL (the DN for the selected object) so the ModifyUser form will allow us to modify the selected user.

1a. Extract the DN information from the URL:

private void Page_Load(object sender, System.EventArgs e)
{
	base.HeaderIconImageUrl = "~/Images/TakeControl.jpg";
			base.HeaderMessage = "Modify Employee Form";

	//set default dn
	string dn = "cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString();
	//get the user dn
	string url = Request.Url.ToString();
	//test if page comes from BrowseTree
	if (url.IndexOf("?dn=")!= -1)
	{
		dn = url.Substring(url.IndexOf("?dn=")+4);
	}

Note that we also consider a case when there is not a DN passed with the URL for the ModifyUser.aspx form. This means we can also call ModifyUser.aspx without providing a DN in the URL. The dn would then be that of the authenticated user built with the gUser and gContext global variables. We will use that later for self-service.

In the above code fragment, notice that we first set the DN to that of the authenticated user. Then we leveraged the IndexOf function to check if the DN is present. If it is, we set its value to the one included in the URL, leveraging the Substring function.

Now that we have the DN, either the one for the authenticated user or the one from the selected object(from BrowseTree),

1b. Query the LDAP directory for current values:

	if (Page.IsPostBack)
		return;

	// If this is an update, preload the values
	if (Context.User.Identity.IsAuthenticated)
	{
		// Change the header message
		base.HeaderMessage = "Update user";

		// Creating an LDAP Connection instance
		LdapConnection ldapConn = new LdapConnection();

		// Connect function will create a socket connection
				ldapConn.Connect(Session["gAddress"].ToString(),Int32.Parse(Session["gPort"].ToString()));

		// Bind function will bind the user object
				ldapConn.Bind("cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString(),Session["gPasswd"].ToString() );

								

		string[] attributeList = new string[]{"cn","givenname","sn","title"};
		//LdapSearchResults lsc = ldapConn.Search("cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString(),
		LdapSearchResults lsc = ldapConn.Search(dn.ToString(),
			LdapConnection.SCOPE_BASE,
			"objectClass=User",
			attributeList,
			false);

		LdapEntry nextEntry = null;
		try
		{
			nextEntry = lsc.next();
		}
		catch (LdapException ex)
		{
			// Exception is thrown to go to next entry
			this.LabelMessage.Visible = true;
			this.LabelMessage.Text = ex.LdapErrorMessage;
		}
		LdapAttributeSet attributeSet = nextEntry.getAttributeSet();
		System.Collections.IEnumerator ienum = attributeSet.GetEnumerator();
		while (ienum.MoveNext())
		{
			LdapAttribute attribute=(LdapAttribute) ienum.Current;
			string attributeName = attribute.Name;
			string attributeVal = attribute.StringValue;
			if (attributeName == "cn") { this.TextLogin.Text = attributeVal ; }
			if (attributeName == "givenname") { this.TextFName.Text = attributeVal ; }
			if (attributeName == "sn") { this.TextLName.Text = attributeVal ; }
			if (attributeName == "title") { this.TextTitle.Text = attributeVal ; }
		}
		 ldapConn.Disconnect();
	}
}

We use the above code to extract the attributes for the selected user, and then we allow the modification of some attributes in the form. Our code is not really complete, but it serves to illustrate concepts more easily than a full blown application. We can still use some attributes like the Title attribute for updates. Let's now add some code based on a click for the ButtonAccept button.

1c. Double-click on the button from the ModifyUser.aspx form, so we are taken back to the code behind form.

1d. Add the following code:

private void ButtonAccept_Click(object sender, System.EventArgs e)
{
	if (Page.IsValid)
	{
		if (Context.User.Identity.IsAuthenticated)
			UpdateUser();
		else
			InsertUser();
	}
	else
	{
		LabelMessage.Text = "Fix the following errors and retry:";
	}
}

private void InsertUser()
{
	if (Page.IsValid)
		LabelMessage.Text = "Validation succeeded!";
	else
		LabelMessage.Text = "Fix the following errors and retry:";
}

We could in theory use the form for both modifying or creating user objects (e.g., Self-Create for new users). But the remainder of this section will be restricted to the modifications of user objects, so I will not complete the logic for creating user objects. I'll leave that to you as a homework assignment ;-) ...

1e. Add the logic for modifying the current user/dn:

private void UpdateUser()
{
	ArrayList modList = new ArrayList();

	// Optional values without quotes as they can be the Null value.
	// Add values to replace
	//	LdapAttribute attrFName = new LdapAttribute("givenname",TextFName.Text);
	// if (TextFName.Text != String.Empty)
	//	modList.Add( new LdapModification(LdapModification.REPLACE,attrFName));

	//	LdapAttribute attrLName = new LdapAttribute("sn",TextLName.Text);
	// if (TextLName.Text != String.Empty)
	//	modList.Add( new LdapModification(LdapModification.REPLACE,attrLName));

	LdapAttribute attrTitle = new LdapAttribute("title",TextTitle.Text);
	if (TextTitle.Text != String.Empty)
		modList.Add( new LdapModification(LdapModification.REPLACE,attrTitle));

	try
	{
		// Creating an LDAP Connection instance
		LdapConnection ldapConn = new LdapConnection();

		// Connect function will create a socket connection
				ldapConn.Connect(Session["gAddress"].ToString(),Int32.Parse(Session["gPort"].ToString()));

		// Bind function will bind the user object
				ldapConn.Bind("cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString(),Session["gPasswd"].ToString());

		LdapModification[] mods = new LdapModification[modList.Count];
		Type mtype=Type.GetType("Novell.Directory.LdapModification");
		mods = (LdapModification[])modList.ToArray(typeof(LdapModification));

		//set default dn
		string dn = "cn="+Session["gUser"].ToString()+","+Session["gContext"].ToString();
		//get the user dn
		string url = Request.Url.ToString();
		//test if page comes from BrowseTree
		if (url.IndexOf("?dn=")!= -1)
		{
			dn = url.Substring(url.IndexOf("?dn=")+4);
		}

		// Modify the entry in the directory
		ldapConn.Modify(dn.ToString(),mods);

		// ldapConn.Disconnect();
	}
	catch (LdapException ex1)
	{
		// Exception is thrown to go to next entry
		this.LabelMessage.Visible = true;
		this.LabelMessage.Text = ex1.LdapErrorMessage;
	}


}

Now you can test the form.

1f. Log in as admin or another user and test the modifications.

1g. Replace the following bloc in SubHeader.cs:

if (_login == string.Empty)
{
	login.NavigateUrl = Context.Request.ApplicationPath +
		Path.AltDirectorySeparatorChar + 
		Path.AltDirectorySeparatorChar + "Login.aspx";
}
else
{
	login.NavigateUrl = _login;
}

with the following:

if (Context.User.Identity.IsAuthenticated)
{
	login.NavigateUrl = Context.Request.ApplicationPath +
		Path.AltDirectorySeparatorChar + 
		Path.AltDirectorySeparatorChar + "ModifyUser.aspx";
}
else
{
	login.NavigateUrl = Context.Request.ApplicationPath +
		Path.AltDirectorySeparatorChar + 
		Path.AltDirectorySeparatorChar + "Login.aspx";
}

This will have the effect of presenting the Login.aspx link in the URL for the SubHeader when the user is not authenticated, or the ModifyUser.aspx link when the user is authenticated. That way, the user can modify his or her own user object (self-service).

Before you run the application again,

2b. Modify the Default.aspx.cs code behind page so the Default page shows the links for ModifyUser.aspx and BrowseTree.aspx.

2c. Set the Start Page back to Default.aspx.

2d. Add the following code within the if statement before the else statement (you can copy and paste the existing row block):

// New Row

// Create a new blank table row
row = new TableRow();

// Set up the Login image
img = new Image();
img.ImageUrl = "Images/TakeControl.jpg";
img.ImageAlign = ImageAlign.Middle;
img.Width = new Unit(24, UnitType.Pixel);
img.Height = new Unit(24, UnitType.Pixel);

// Create a cell and add the image
cell = new TableCell();
cell.Controls.Add(img);

// Add the new cell to the row
row.Cells.Add(cell);

// Set up the Login link
lnk = new HyperLink();
lnk.Text = "Modify your Profile";
lnk.NavigateUrl = "ModifyUser.aspx";

//Create the cell and add the link
cell = new TableCell();
cell.Controls.Add(lnk);

// Add the new cell to the row
row.Cells.Add(cell);

// Add the row to the table
tb.Rows.Add(row);

// Create a new blank table row
row = new TableRow();


// New Row

// Create a new blank table row
row = new TableRow();

// Set up the Login image
img = new Image();
img.ImageUrl = "Images/tree32a.jpg";
img.ImageAlign = ImageAlign.Middle;
img.Width = new Unit(24, UnitType.Pixel);
img.Height = new Unit(24, UnitType.Pixel);

// Create a cell and add the image
cell = new TableCell();
cell.Controls.Add(img);

// Add the new cell to the row
row.Cells.Add(cell);

// Set up the Login link
lnk = new HyperLink();
lnk.Text = "Browse the tree";
lnk.NavigateUrl = "BrowseTree.aspx";

//Create the cell and add the link
cell = new TableCell();
cell.Controls.Add(lnk);

// Add the new cell to the row
row.Cells.Add(cell);

// Add the row to the table
tb.Rows.Add(row);

// Create a new blank table row
row = new TableRow();

1h. Run the application and access the default page, which should look like this:

Figure 6.1: Default.aspx page with a self-service link(Modify your Profile) and the link for BrowseTree.aspx

Note that the Edit your profile SubHeader link now points to ModifyUser.aspx.

Figure 6.1a: ModifyUser.aspx page

Figure 6.1b: BrowseTree.aspx page

If the user selects the entry/dn for another user, he or she is presented with the ModifyUser.aspx form for that dn. With proper rights assigned, this form can be used to manage user objects.

7. Moving the Application to a Linux Machine using Mono and XSP

Now we have a basic application that illustrates many concepts that are important for building C# web applications on top of LDAP. There is one important point to emphasize: since we do not depend on Microsoft SQL Server for data (ADO) or security, and since we do not depend on the Windows platform, we should be able to move our application to another platform like Linux or Mac using the Mono platform and XSP runtime.

Just how easy is that? How about a simple copy and paste!

Even with the relative complexity in our demo application, we can still get away with a basic copy and paste. But first, we need to install Mono on Linux (or Mac if you have one handy). Mono can be downloaded from the http://www.go-mono.com web site. An installer is now available for various platforms, including Windows.

Figure 7.1: Mono download page

Once the XSP runtime is available, you can copy the application onto the Linux machine from c:\inetpub\wwwroot\LdapApplication1. I copied LdapApplication1 under /home/root/dotnet. Then you can invoke the command to start the XSP engine from a terminal window:

Figure 7.2: Loading the XSP engine from a Terminal Window on SUSE Linux Enterprise Server 9

Figure 7.3: Accessing LdapApplication1 using Firefox on SLES9

Figure 7.4: Accessing the Default page using Firefox on SLES9

Figure 7.5: Accessing the BrowseTree page using Firefox on SLES9

Figure 7.6: Accessing the ModifyUser page using Firefox on SLES9

Conclusion

We have created a basic C# visual web application that is multi-platform, and that sits on top of a LDAP Directory. Through this exercise, we learned:

  • A few tricks for interfacing with LDAP (authentication, search, query for attributes, modify).
  • A way (there are other ways) to save our credentials inside global variables so the user does not have to authenticate for each and every form(single sign-on).
  • How to use inline parameters inside the URL to pass information between forms.

As a last step, we tested our application against a Linux machine, leveraging Mono and XSP, to demonstrate that our application does not have any dependencies on the Windows platform or on a data store such as Microsoft SQL Server.

While the application we built is not meant for a replacement to iManager or any other existing management console, the techniques we used here can be leveraged in your existing .NET applications when interfacing with a LDAP Directory such as Novell eDirectory.


Novell Cool Solutions (corporate web communities) are produced by WebWise Solutions. www.webwiseone.com

© 2014 Novell