LDAP provider sample

Contents[Hide]

1. Overview

This sample shows how to create a simple LDAP accounts provider. This sample is intended as an extension of the Create a custom accounts provider article and assumes you are familiar with it. You will learn the following:

  • How to create a simple LDAP accounts provider
  • How to create custom application configuration settings

2. Getting started

For Dundas BI version 10 and higher, the following prerequisites must be installed on your computer to build the provided sample without modifications:

  • Visual Studio 2022 or higher
  • Microsoft .NET 6

For Dundas BI version 9 and earlier installed on Windows, the following prerequisites must be installed on your computer to build the provided sample without modifications:

  • Visual Studio 2017 or higher
  • Microsoft .NET Framework 4.7.2

Note
This sample uses the LdapConnection class and related functionality from the .NET Framework and .NET 6. If your Dundas BI instance is not running on Windows, upgrade to version 10 or higher to be able to run this sample.

2.1. Downloading the sample solution

To download the LDAP custom accounts provider sample solution, click on the link below for your version of Dundas BI:

(A sample solution is also available for Dundas BI version 6, and version 5.)

2.2. Opening the solution

Extract LdapProviderSample.zip to a folder and open the solution in Microsoft Visual Studio to build the extension.

To use the option to publish directly to your Dundas BI instance, run Visual Studio as an administrator before opening the solution. For example, you can right-click Visual Studio in the start menu and find the Run as administrator option.

The solution file is located at:
[Extracted folder]\LdapProviderSample\LdapProviderSample.sln

3. The project

The project is a class library.

  • LdapProvider.cs - This class contains the implementation of the LDAP custom account provider.
  • LdapProviderSamplePackageInfo.cs - This class contains information about the extension package, and contains the loaded event that allows for application configuration settings to be added to Dundas BI.
  • PublishExtensionTemplate.props - Used for auto publishing the extension after the build succeeds, and defines extension properties, and files.

3.1. Register application configuration settings

The LDAP provider needs a few properties to be configured:

  • LDAP Distinguished Name
  • LDAP Server Path
  • LDAP Query Username
  • LDAP Query Password

To accomplish this, we need to override the OnLoaded method of the ExtensionPackageInfo2 class.

using Dundas.BI;
using Dundas.BI.Configuration;
using Dundas.BI.Extensibility;
   ...

/// <summary>
/// Called after the extension package is loaded during engine startup.
/// </summary>
public override void OnLoaded()
{
    LdapProvider.RegisterSettings();
}

On the provider itself is the code that registers the application configuration settings:

using Dundas.BI;
using Dundas.BI.AccountServices;
using Dundas.BI.AccountServices.Extensibility;
using Dundas.BI.Configuration;
using Dundas.BI.Licensing;
   ...

private static Guid ldapDistinguishedNameId = new Guid("e370e76d-8eb2-44b9-b59f-a3fb1eae99ef");
private static Guid ldapServerPathId = new Guid("630d1ae8-43fb-4d7c-acb2-d3558823e4f5");
private static Guid ldapQueryUsernameId = new Guid("3ff30651-fa6a-41d3-bcfb-54c1d5843025");
private static Guid ldapQueryPasswordId = new Guid("8bc5b031-da8b-4959-9c25-556b45d4df1c");

/// <summary>
/// Registers the settings for the LDAP provider.
/// </summary>
public static void RegisterSettings()
{
	IAppConfigService appConfigService = Engine.Current.GetService<IAppConfigService>();

	AppSettingProperties appSettingsProperties = new AppSettingProperties(
        ldapDistinguishedNameId, 
        "LdapDistinguishedName", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );
    appSettingsProperties.Description = string.Concat(
        "Enter the LDAP Distinguished Name.  ",
        "e.g.:  OU=Interactive Users,OU=Your Company,DC=yourcompany,DC=com"
    );
                
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapServerPathId, 
        "LdapServerPath", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );

	appSettingsProperties.Description = string.Concat(
		"Enter the LDAP server path.  ",
		"e.g.:   dc02.yourcompany.com"
	);
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapQueryUsernameId, 
        "LdapQueryUsername", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
	);
    appSettingsProperties.Description = string.Concat(
        "Enter the LDAP query username.  ",
        "e.g.: ",
        "CN=User A,OU=Interactive Users,OU=Your Company,DC=yourcompany,DC=com"
    );
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);

	appSettingsProperties = new AppSettingProperties(
        ldapQueryPasswordId, 
        "LdapQueryPassword", 
        LdapProviderSamplePackageInfo.PackageId, 
        typeof(string)
    );
	appSettingsProperties.Description = "Enter the LDAP query password.";
	appSettingsProperties.IsEncrypted = true;
	appSettingsProperties.ClientValueVisibility = AppSettingValueVisibility.SystemAdministrator;
	appSettingsProperties.IsPassword = true;
	appSettingsProperties.DefaultValue = string.Empty;
	appConfigService.RegisterSetting(appSettingsProperties);
}

Now the application configuration settings are available to our provider:

private string ldapDistinguishedName
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapDistinguishedNameId);
	}
}

private string ldapQueryPassword
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapQueryPasswordId);
	}
}

private string ldapQueryUsername
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapQueryUsernameId);
	}
}

private string ldapServerPath
{
	get
	{
		return Engine.Current.GetService<IAppConfigService>().GetString(ldapServerPathId);
	}
}

3.2. Define interface members that are handled by LDAP

In the case of this simple LDAP accounts provider, many of the interface members are handled by LDAP. We do not want to handle them in this case. The following code demonstrates this:

public bool IsModifySupported
{
    get { return false; }
}

public void DeleteRecord(Guid recordId)
{
    throw new NotSupportedException(string.Concat(
        "Accounts should be deleted through the LDAP server",
        " and not through Dundas BI."
    ));
}

public AccountData ExportRecord(Guid accountId)
{
	throw new NotSupportedException(string.Concat(
        "Accounts should be exported through the LDAP server", 
        " and not through Dundas BI."
    ));
}

public bool ImportRecord(AccountData record)
{
    throw new NotSupportedException(string.Concat(
        "Accounts should be created through the LDAP server",
        " and not through Dundas BI."
    ));
}

public void SetLocalUserAccountNewPassword(Guid accountId, string newPassword)
{
	throw new NotSupportedException(string.Concat(
        "Passwords should be managed through the LDAP server",
        "and not through Dundas BI."
    ));
}

public void UpdateDynamicAccountProperties(Guid accountId, DynamicAccountProperties properties)
{
    // Accounts should be modified through the LDAP server 
    // and not through Dundas BI.
}

public void UpdateLogOnFailureInfo(Guid accountId, int failedLogOnCount, DateTime? lockedUntil)
{
    // This method is not called in IAccountsProvider2, 
    // is replaced by UpdateDynamicAccountProperties
}


public void UpdateLogOnTimestampAndCount(Guid accountId, DateTime logOnTimestamp)
{
    // This method is not called in IAccountsProvider2, 
    // is replaced by UpdateDynamicAccountProperties
}

public void ResetFailedLogOnInfo(Guid accountId)
{
    // Failed logons should be modified through the LDAP server
    // and not through Dundas BI.
}

public Guid SaveRecord(AccountData record)
{
    throw new NotSupportedException(string.Concat(
        "Account Data should be managed through the LDAP server",
        " and not through Dundas BI."
    ));
}
	

3.3. Connecting to LDAP and retrieving accounts

The following code connects to the LDAP server and retrieves all the account data:

private AccountData GetAccountData(string ldapFilter)
{
	ICollection<AccountData> accountDataCollection = GetAccountDataCollection(ldapFilter);

	if (accountDataCollection.Count.Equals(0))
	{
		return null;
	}
	else
	{
		return accountDataCollection.First();
	}
}

private ICollection<AccountData> GetAccountDataCollection(string ldapFilter)
{
	ICollection<AccountData> accountDataCollection = new Collection<AccountData>();

	using (LdapConnection ldapConnection = new LdapConnection(this.ldapServerPath))
	{
		NetworkCredential queryCredential = new NetworkCredential(
			this.ldapQueryUsername,
			this.ldapQueryPassword
		);

		ldapConnection.AuthType = AuthType.Basic;

		try
		{
			ldapConnection.Bind(queryCredential);
			SearchRequest request = new SearchRequest(
				this.ldapDistinguishedName,
				ldapFilter,
				SearchScope.Subtree,
				accountDataAttributesToReturn
			);

			SearchResponse searchResponse = (SearchResponse)ldapConnection.SendRequest(request);

			foreach (SearchResultEntry entry in searchResponse.Entries)
			{
				accountDataCollection.Add(GetAccountDataFromSearchResultEntry(entry));
			}

		}
		catch (DirectoryOperationException ex)
		{
			throw new ArgumentException(string.Concat(
				"Query authentication failed for reasons ",
				"other than invalid credentials: ",
				ex.Message,
				"  Please ensure your LDAP server is available and",
				" your query account is a member of at least one user group."
			));
		}
		catch (LdapException ex)
		{
			throw new ArgumentException(string.Concat(
				"The LDAP server did not accept the query connection: ",
				ex.Message
			));
		}

		return accountDataCollection;
	}
}

/// <summary>
/// Gets the account data from search result entry.
/// </summary>
/// <param name="searchResultEntry">The search result entry.</param>
/// <returns></returns>
private AccountData GetAccountDataFromSearchResultEntry(SearchResultEntry entry)
{
	string stringTime;
	DateTime timeCreated = DateTime.MinValue;
	if (entry.Attributes["createTimeStamp"] != null)
	{
		stringTime = entry.Attributes["createTimeStamp"].GetValues(typeof(String))[0].ToString();
		timeCreated = new DateTime(
			Int32.Parse(stringTime.Substring(0, 4)),
			Int32.Parse(stringTime.Substring(4, 2)),
			Int32.Parse(stringTime.Substring(6, 2)),
			Int32.Parse(stringTime.Substring(8, 2)),
			Int32.Parse(stringTime.Substring(10, 2)),
			Int32.Parse(stringTime.Substring(12, 2))
		);
	}
	DateTime lastLogon = DateTime.MinValue;
	if (entry.Attributes["lastLogonTimestamp"] != null)
	{
		stringTime = entry.Attributes["lastLogonTimestamp"].GetValues(typeof(String))[0].ToString();
		lastLogon = DateTime.FromFileTimeUtc(long.Parse(stringTime));
	}
	DateTime? accountExpires = null;
	if (entry.Attributes["accountExpires"] != null)
	{
		stringTime = entry.Attributes["accountExpires"].GetValues(typeof(String))[0].ToString();
		long expiresAsLong = long.Parse(stringTime);

		if (expiresAsLong.Equals(Int64.MaxValue))
		{
			accountExpires = null;
		}
		else
		{
			accountExpires = DateTime.FromFileTimeUtc(expiresAsLong);
		}
	}
	string mail = String.Empty;
	if (entry.Attributes["mail"] != null)
	{
		mail = entry.Attributes["mail"].GetValues(typeof(String))[0].ToString();
	}
	string givenName = String.Empty;
	if (entry.Attributes["givenName"] != null)
	{
		givenName = entry.Attributes["givenName"].GetValues(typeof(String))[0].ToString();
	}
	String surname = String.Empty;
	if (entry.Attributes["userPrincipalName"] != null)
	{
		surname = entry.Attributes["userPrincipalName"].GetValues(typeof(String))[0].ToString();
	}
	bool accountIsEnabled = true;

	string returnedUsername = String.Empty;
	if (entry.Attributes["name"] != null)
	{
		returnedUsername = entry.Attributes["name"].GetValues(typeof(String))[0].ToString();
	}

	byte[] id = new byte[0];
	if (entry.Attributes["objectGUID"] != null)
	{
		id = (byte[])entry.Attributes["objectGUID"].GetValues(typeof(byte[]))[0];
	}

	LicenseSeatKind licenseSeatKind = LicenseSeatKind.StandardUser;
	bool isSeatReserved = false;

	if (entry.Attributes["department"] != null)
	{
		if (entry.Attributes["department"].GetValues(typeof(String))[0]
			.ToString().Equals("Product Development"))
		{
			licenseSeatKind = LicenseSeatKind.Developer;
			isSeatReserved = true;
		}
	}

	string returnedDisplayName = String.Empty;
	if (entry.Attributes["displayName"] != null)
	{
		returnedDisplayName = entry.Attributes["displayName"].GetValues(typeof(String))[0].ToString();
	}


	LocalUserAccountData accountData = new LocalUserAccountData();
	accountData.CreatedTime = timeCreated;
	accountData.LastLogOnTime = lastLogon;
	accountData.LogOnCount = 0;
	accountData.AccountExpiryDate = accountExpires;
	accountData.CanChangePassword = false;
	accountData.EmailAddress = mail;
	accountData.IsEnabled = accountIsEnabled;
	accountData.DisplayName = returnedUsername;
	accountData.SeatKind = licenseSeatKind;
	accountData.IsSeatReserved = isSeatReserved;
	accountData.DisplayName = returnedDisplayName;
	accountData.Name = returnedUsername;
	accountData.Id = new Guid(id);

	return accountData;
}

/// <summary>
/// Gets all the user records.
/// </summary>
/// <returns>
/// An IEnumerable of 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public IEnumerable<AccountData> GetAllRecords()
{
	return GetAccountDataCollection("(objectClass=user)").ToList();
}

/// <summary>
/// Retrieves a list of 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// having the specified IDs.
/// </summary>
/// <param name="recordIds">
/// The IDs of the records to retrieve.
/// </param>
/// <returns>
/// The requested records as 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />.
/// </returns>
public ICollection<AccountData> GetById(ICollection<Guid> recordIds)
{
	ICollection<AccountData> accountDataList = new Collection<AccountData>();

	foreach (Guid id in recordIds)
	{
		string ldapFilter = string.Format(
			CultureInfo.InvariantCulture,
			"(objectGUID={0})",
			id.ToLdapObjectGuidFormattedString()
		);

		AccountData accountData = GetAccountData(ldapFilter);

		if (accountData == null)
		{
			throw new NotFoundException(string.Format(
				CultureInfo.CurrentCulture,
				"Unable to find user with ID = {0}",
				id.ToString()
			));
		}
		else
		{
			accountDataList.Add(accountData);
		}
	}

	return accountDataList;
}

/// <summary>
/// Retrieves an account specified by its name.
/// </summary>
/// <param name="accountName">The account's name.</param>
/// <returns>
/// The 
/// <see cref="T:Dundas.BI.AccountServices.Extensibility.AccountData" />
/// or <see langword="null" /> if it does not exist.
/// </returns>
public AccountData GetByName(string accountName)
{
	string filter = string.Format(
		CultureInfo.InvariantCulture,
		"(&(objectClass=user)(name={0}))",
		accountName
	);

	return GetAccountData(filter);
}

3.4. Logging into LDAP

The following code validates the passed logon credentials against the specified LDAP server.

/// <summary>
/// Validates local user account logon credentials.
/// </summary>
/// <param name="context">The logon context.</param>
/// <returns>
///   <see langword="true" /> 
///   if the credentials are valid; otherwise, 
///   <see langword="false" />.
/// </returns>
/// <exception cref="System.ArgumentNullException">context</exception>
public bool ValidateLocalUserLogOnCredentials(LocalLogOnContext context)
{
	// Param validation.
	if (context == null)
	{
		throw new ArgumentNullException("context");
	}

	LogOnCredential usernameCredential = 
        context.Credentials.First(c => c.Id == LogOnCredentialIds.AccountName);
	LogOnCredential passwordCredential = 
        context.Credentials.First(c => c.Id == LogOnCredentialIds.Password);

	// Treats the username as the common name 
    // and creates a distinguished name for the user
	String userNameAndDistinguishedName =
		string.Concat("cn=", usernameCredential.Value, ",", this.ldapDistinguishedName);

	NetworkCredential ldapCredentials = 
        new NetworkCredential(userNameAndDistinguishedName, passwordCredential.Value);

	using (LdapConnection ldapConnection = new LdapConnection(this.ldapServerPath))
	{
		ldapConnection.AuthType = AuthType.Basic;

		try
		{
			ldapConnection.Bind(ldapCredentials);
		}
		catch
		{
			// If the bind threw an exception 
            // then something went wrong and the account wasn't authenticated
			return false;
		}

		return true;
	}
}

4. See also

Dundas Data Visualization, Inc.
400-15 Gervais Drive
Toronto, ON, Canada
M3C 1Y8

North America: 1.800.463.1492
International: 1.416.467.5100

Dundas Support Hours:
Phone: 9am-6pm, ET, Mon-Fri
Email: 7am-6pm, ET, Mon-Fri