首页    期刊浏览 2024年11月25日 星期一
登录注册

文章基本信息

  • 标题:Building Plug-ins with C# .NET Part 3: Adding LDAP, SQL, and Configuration
  • 作者:Nathan Good
  • 期刊名称:Dev Source
  • 出版年度:2004
  • 卷号:June 2004
  • 出版社:DEVsource

Building Plug-ins with C# .NET Part 3: Adding LDAP, SQL, and Configuration

Nathan Good

This is the third article in a four-part series on building plug-ins with C# .NET. The second part in the series covered some XML-related topics, such as using an XML file to store usernames and passwords, and writing a custom XML configuration section handler to parse the application configuration file.

In this article, I'll show you how to build two new plug-ins that require configuration. The first plug-in requires an LDAP root and the name of a domain in which to find a user. The second requires a database connection string. Aside from the configuration concepts, the other concepts that I'll introduce are using directory services like LDAP to authenticate against Active Directory, and using classes in the System.Data.SqlClient namespace to connect to a database.

Configuration Issues

When the plug-ins are loaded by the testing app, AuthenticationPlugin.UI.UITester, a call to ConfigurationSettings.AppSettings will pull configuration values from the application's configuration file, even if an XML configuration file was added correctly to the plug-in project. This can be seen by the Debug statement added to the MyTest test plug-in, which prints the location of the configuration file using the built-in .NET AppDomain class:

Debug.WriteLine
  ( AppDomain.CurrentDomain.SetupInformation.ConfigurationFile );

The Debug statement above prints the full name of the file that is being used as the configuration file, which will be the Application's configuration file.

This presents a slight problem. To add configurable values to one of the plug-ins, I could simply add the configuration keys to the application file, and I could even keep the file separate but include it using:

<appSettings file="plugin.config" />

However, with many different plug-ins, the potential exists for configuration keys to overlap. For instance, if more than one plug-in uses a database connection, I could easily have two configuration strings. One way to get around this is to add the full namespace and classname to the front of the configuration key, like this:

<appSettings>
  <add key="AuthenticationPlugin.Plugins.SqlAuthenticator.connectionString"
  value="Database=…" />
  </appSettings>

Although this can work fine, the main reason why you might not want to configure the plug-ins this way is because it requires you to modify the application's configuration file. A requirement to add values to the configuration file conflicts with the intended concept: add the plug-in, and use it right away.

To solve this configuration dilemma, I wrote a base class that can be extended by any plug-in that requires configuration. Using a base class allows both our two new plug-ins, the LDAP and database authentication plug-ins, to take advantage of the same code to load configuration.

The base class uses an XML reader to iterate through a file and add any elements it finds under the base element to a NameValueCollection—a class that comes with .NET in the Systems.Collections.Specialty namespace. The base class uses the Assembly's current directory and name to compose a full configuration file filename by tacking on an extension. I chose ".config" as the file extension, but you could change it to make more sense for your implementation.

The LoadConfiguration method from the base class is shown in Listing 1-1. The LoadConfiguration method accepts the name of the configuration file's root node as a string and returns a new NameValueCollection object with the element names and their values. A sample file, listed here, results in a NameValueCollection with two key value pairs in it: valueA, with the value of Foo, and valueB with the value of Bar.

<?xml version="1.0"?>
<rootElement>
   <valueA>Foo</valueA>
   <valueB>Bar</valueB>
</rootElement>

An XMLTextReader reads in the configuration file. The reader iterates through the XML nodes and looks to see if the node is of type Element (other types include Comment, Document, Text, and more). Once an Element is found (otherwise known as a tag), the name of the element is added to the NameValueCollection along with the text inside the element. The root element, identified by the single parameter to the LoadConfiguration method, is skipped because we don't want it in the configuration key-value pair.

The base class is added to the AuthenticationPlugin.Common project so other plug-ins can extend from it easily; they will all need to refer to the project anyway, to implement the IAuthenticationPlugin interface and define the AuthenticationPlugin attribute.

Listing 1-1

protected NameValueCollection LoadConfiguration( string rootNodeName )
{

  NameValueCollection config = new NameValueCollection();
  /* It's not implemented for this example for the sake of
   * brevity, but this method really should check to see 
   * if the file exists and log the error somewhere if it 
   * doesn't.
   */
  Debug.WriteLine( Assembly.GetCallingAssembly().Location );
  XmlTextReader configReader = new XmlTextReader( 
          Assembly.GetCallingAssembly().Location + ".config" );

    try
    {
          
    /* While the file is not at the end */
    while ( ! configReader.EOF )
    {
           /* In this specific configuration file, the code is 
            * looking for elements 
            */
           if ( XmlNodeType.Element == configReader.MoveToContent() &&
                   configReader.Name != rootNodeName )
           {
           /* Add the configuration to the internal 
              NameValueCollection object */
           config.Add( configReader.Name, 
           configReader.ReadElementString() );
           }
           else
           {
           /* Advance through the file if we're not interested in
            * the current node
            */
            configReader.Read();
            }
    }
 }
  finally
  {
    /* It is very important to close the file */
    if ( null != configReader )
    {
    configReader.Close();
    }
  }
  return config;
}

The LdapAuthenticator

A plug-in using LDAP authentication, which can be used to authenticate a user against Active Directory, is the first plug-in added to the solution. It extends the XmlConfiguredPlugin class along with implementing the IAuthenticationPlugin interface. The new project is called AuthenticationPlugin.Plugins.LdapAuthenticator. A project reference to the AuthenticationPlugins.Common project is added right away to allow the project to compile.

In Listing 1-2, you'll see the IsLoginValid method from the newly-created AuthenticationPlugin.Plugins.LdapAuthenticator.Plugin class. The call to the base class' LoadConfiguration method gets the NameValueCollection, which is used later in the IsLoginValid method to find the LDAP server and to get a domain name.

The classes needed for authenticating users with LDAP are in the System.DirectoryServices namespace, so using System.DirectoryServices needs to be added to the Plugin class as well as a reference to the System.DirectoryServices.dll assembly. The LDAP string in the configuration file looks like LDAP://dc=domain,dc=com if your Active Directory domain is called domain.com.

The NativeObject call on the DirectoryEntry object entry is an attempt to bind to the object in the directory. Since this call forces authentication, you will get an error if the user does not exist. If the user is a valid user in the domain, the call will succeed. Unfortunately, since the call throws an exception, there is a bit of a performance hit if the user is not a valid user.

Listing 1-2.

public bool IsLoginValid( string username, string password )
{
   bool isValid = false;

   try
   {
      
      NameValueCollection configuration = 
         LoadConfiguration( ConfigurationRootNodeName );

      /* Get the domain name from the assembly-specific configuration 
       * file
       */
      string domainName = configuration[ "domain" ];
      string ldapPath = configuration[ "ldapPath" ];
      Debug.WriteLine( string.Format( "LDAP path is '{0}'", 
         ldapPath ) );

      /* The full username for LDAP will also have the domain
       * with it
       */
      string fullUsername = string.Format( @"{0}\{1}", 
         domainName, username );
      Debug.WriteLine( string.Format( "Full user name is '{0}'", 
         fullUsername ) );

      DirectoryEntry entry = new DirectoryEntry( ldapPath, 
         fullUsername, password );
      
      /* This will actually force authentication, and will throw an 
       * exception 
       * if the user and password are unknown or invalid
       */
      object obj = entry.NativeObject;

      /* Just making sure at this point */
      DirectorySearcher searcher = new DirectorySearcher( entry );
      searcher.Filter = string.Format( "(samAccountName={0})", 
         username );
      SearchResult result = searcher.FindOne();

      isValid = ( null != result );

   }
   catch
   {
      /* Replace this code with logging, etc. */
   }

   return isValid;
}

Database authentication

I put the second plug-in into a new project called AuthenticationPlugin.Plugins.SqlAuthenticator. This plug-in authenticates a user against values in a database. Similar to the LdapAuthenticator plug-in, it requires some configuration for things like the database connection string. The configuration file is shown here:

<sqlAuthenticator>
  <connectionString>Database=;...</connectionString>
</sqlAuthenticator>

When the LoadConfiguration method is called in the IsLoginValid method, the configuration file loads into the NameValueCollection returned by the LoadConfiguration method. The value of the key is accessed later in the IsLoginValid method:

string connectionString = configuration[ "connectionString"];

That key is used when creating a new instance of the SqlConnection class from the System.Data.SqlClient namespace. For the most part, this is pretty standard database connection code—the only thing that might look odd to you (if you aren't used to seeing it) is the using statement in which the SqlConnection object is created.

The using statement makes sure that any class that implements the IDisposable interface is properly closed and disposed of at the end of the block, regardless of whether an exception occurred. This is the same as building a try...finally block and putting a conn.Close() line inside finally to make sure that even if an exception happens the database connection is closed. The using statement is a little more brief and cleaner than the try...finally block.

The result value of the method, isValid, is finally assigned to true if the string result (retrieved by the ExecuteScalar method which returns the first column of the first row found).

Summary

The downloadable solution contains two new projects: AuthenticationPlugin.Plugins.LdapAuthenticator and AuthenticationPlugin.Plugins.SqlAuthenticator. Both contain a Plugin class with the main implementation code of the plug-in. They also contain example configuration files. The AuthenticationPlugin.Plugins.SqlAuthenticator project has a file containing SQL, dbschema.sql, that you can use to create a database and a stored procedure to test the database authentication plug-in.

The key concepts discussed in this article were using inheritance to re-use code. I also showed you how plug-ins can load their own configurations, so applications that use them don't have to worry about whether the plug-in was configured correctly. Finally, I very briefly touched on two concepts: using the classes in the System.DirectoryServices namespace to do LDAP authentication, and using classes in the System.Data.SqlClient namespace to connect to a database and run a stored procedure.

Copyright © 2004 Ziff Davis Media Inc. All Rights Reserved. Originally appearing in Dev Source.

联系我们|关于我们|网站声明
国家哲学社会科学文献中心版权所有